Skip to content

perf: add 4 CodSpeed scenarios for cold-start work (issue #557)#12

Merged
Tagar merged 1 commit into
masterfrom
pr-0-cold-start-metrics
May 22, 2026
Merged

perf: add 4 CodSpeed scenarios for cold-start work (issue #557)#12
Tagar merged 1 commit into
masterfrom
pr-0-cold-start-metrics

Conversation

@Tagar

@Tagar Tagar commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Adds 4 CodSpeed scenarios so the planned cold-start improvement PRs (per upstream issue py4j/py4j#557) each get an unambiguous per-PR delta on the dashboard. Pure measurement infra — zero behavior change in py4j itself.

Each scenario maps 1:1 to a planned PR:

Scenario Targets which PR What it measures
XA-3 Python attribute-cache PR 1 000 walks of gateway.jvm.java.lang.System. Each walk re-issues 3 reflection round-trips today because JVMView/JavaPackage/JavaClass.__getattr__ don't memoize. Should collapse to ~0 RTTs after the first walk once memoization lands.
XB Java command-prototype cache + background warm 50 fresh JavaGateway() instances against a running JVM. Measures per-connection setup cost — Java accept loop allocates 14 command class instances per connection (GatewayConnection.java:196-208).
XC Lazy CallbackClient PR X1-1 workload (10 000 currentTimeMillis() calls) with CallbackServer started but no callbacks invoked. Delta vs X1-1 = steady-state overhead of the callback infrastructure.
XD JVM-flag opt-ins (-XX:TieredStopAtLevel=1, AppCDS, CRaC) subprocess.Popen → first call → shutdown, one cycle per round. Full cold start.

Local measurements (M-series + JDK 21 / Temurin 21.0.10)

ID Median Per-op Notes
XA-3 94 ms 94 µs/walk 3 reflection RTTs per walk
XB 21 ms 425 µs/reconnect per-connection setup
XC 236 ms 23.6 µs/call ~28% overhead vs X1-1 baseline
XD 355 ms 355 ms/cycle one full cold start

Implementation notes

  • XA / XB / XC follow the existing MacroScenario contract and reuse the macro_gateway fixture in codspeed_macros.py (shared JVM, setup once, measure many).
  • XD owns its JVM lifecycle inside measure() — it must NOT share the fresh_jvm()-managed fixture because both default to port 25333. So XD is registered in a new _COLD_START_SCENARIOS list parametrized through a new test_cold_start_scenario function, and is omitted from ALL_MACRO_CLASSES so the framework runner doesn't try to share its JVM either.

Test plan

  • All 4 scenarios run cleanly via pytest-benchmark locally (~7 s + 3.5 s)
  • Existing macro scenarios (X1–X8) unchanged
  • python -m py4j.tests.perf --only XA-3,XB,XC passes through the framework runner
  • Existing perf-framework self-tests still pass (87 passed, 1 skipped)
  • CodSpeed CI picks up XA/XB/XC/XD as 4 new tracked benchmarks on master

Each scenario maps 1:1 to a planned cold-start improvement so a per-
PR CodSpeed delta is unambiguous. Without these, the wins from the
upcoming PRs would be invisible to the dashboard.

* XA-3 — attribute_walk_jvm_java_lang_System
  1 000 walks of `gateway.jvm.java.lang.System`. Each walk re-issues
  3 reflection round-trips because JVMView / JavaPackage / JavaClass
  __getattr__ don't memoize today. Cache canary for the upcoming
  Python attribute-cache PR — should collapse to ~0 round-trips
  after first walk once memoization lands.

* XB — gateway_reconnect_against_running_jvm
  50 fresh JavaGateway() instances against the fixture JVM. Measures
  per-connection setup cost: Java accept() loop allocates 14 command
  class instances per connection (GatewayConnection.java:196-208),
  Python runs _create_connection + socket setup. Lever for the
  Java-side command-prototype-cache PR.

* XC — callback_infra_overhead_unused
  X1-1 workload (10 000 currentTimeMillis() calls) with
  CallbackServer started but no callbacks invoked. Delta vs X1-1
  is the steady-state overhead of running the callback server
  alongside the main gateway. Target of the lazy-CallbackClient PR.

* XD — full_cold_start_subprocess_first_call
  subprocess.Popen -> first call -> shutdown, one cycle per round.
  Slow per-iteration (~350 ms on M-series + JDK 21, multi-second on
  slower hosts); CodSpeed adapts iteration count. Target of the
  JVM-flag opt-ins (TieredStopAtLevel=1, AppCDS, CRaC).

Implementation notes:

XA / XB / XC follow the existing MacroScenario contract and run
through the macro_gateway fixture in codspeed_macros.py (shared JVM,
setup once, measure many).

XD owns its JVM lifecycle inside measure() — it must NOT share the
fresh_jvm()-managed fixture because both default to port 25333. So
XD is registered in a new _COLD_START_SCENARIOS list parametrized
through a new test_cold_start_scenario function. XD is also omitted
from ALL_MACRO_CLASSES so the framework runner doesn't try to share
its JVM either.

Local measurements on M-series + JDK 21 (Temurin 21.0.10):
  XA-3: ~94 ms / 1000 walks = 94 us per walk (3 RTTs each)
  XB:   ~21 ms / 50 reconnects = 425 us per reconnect
  XC:   ~236 ms / 10k calls = 23.6 us per call
  XD:   ~355 ms per full cold-start cycle

Co-authored-by: Isaac
@Tagar
Tagar merged commit eb4e496 into master May 22, 2026
1 check failed
Tagar added a commit that referenced this pull request May 22, 2026
The XD ``test_cold_start_scenario`` introduced in #12 used a blind
``time.sleep(0.25)`` followed by a direct ``JavaGateway()`` connect
attempt. On CodSpeed CI runners (and any host slower than a modern
laptop) the JVM hadn't yet bound its listen socket when the connect
fired, surfacing as ``ConnectionRefusedError: [Errno 111] Connection
refused`` and failing the whole codspeed workflow.

Switch XD's body to the existing ``fresh_jvm`` context manager with
``readiness_retries=5``. ``fresh_jvm`` polls connection readiness
with ``check_connection`` and only proceeds once the JVM is actually
accepting calls — same convention the rest of the test suite uses.
This also drops a bunch of try/finally boilerplate since
``fresh_jvm`` handles spawn/shutdown.

Locally on M-series + JDK 21: XD median ~355 ms (unchanged from
post-#12 baseline). The variance is higher in cold-start measurement
by nature; what matters is the elimination of hard failures on
slower hosts.

Co-authored-by: Isaac
Tagar added a commit to py4j/py4j that referenced this pull request May 28, 2026
#557) (#595)

Adds four CodSpeed scenarios mapping 1:1 to py4j cold-start improvement
work areas, plus tightens the perf-framework JVM readiness check so
slow scenarios get enough samples per benchmark budget.

Pure measurement infrastructure — zero behavior change in py4j proper.
Touches only the perf testing subtree.

New scenarios:
* XA-3 — attribute_walk_jvm_java_lang_System
    1 000 walks of ``gateway.jvm.java.lang.System``. Each walk
    re-issues 3 reflection round-trips today because JVMView /
    JavaPackage / JavaClass __getattr__ don't memoize. Cache canary
    for the upcoming Python attribute-cache PR.
* XB — gateway_reconnect_against_running_jvm
    50 fresh ``JavaGateway()`` against the fixture JVM per round.
    Measures per-connection setup cost — Java accept() loop allocates
    14 command class instances per connection
    (GatewayConnection.java:196-208), Python runs _create_connection
    + socket setup.
* XC — callback_infra_overhead_unused
    X1-1 workload with ``CallbackServer`` started, but no callbacks
    actually invoked. Delta vs X1-1 = the steady-state cost of
    running callback infrastructure alongside the main gateway.
* XD — full_cold_start_subprocess_first_call
    subprocess.Popen -> first call -> shutdown, one cycle per round.
    Slow per-iteration; CodSpeed adapts iteration count. Target of
    the JVM-flag work (AppCDS, -XX:TieredStopAtLevel=1, CRaC).

XA/XB/XC share the long-running fixture JVM (``test_macro_scenario``).
XD owns its full JVM lifecycle inside ``measure()`` and is dispatched
via a separate ``test_cold_start_scenario`` test function — it can't
share the fixture because both default to port 25333.

Readiness model overhaul in ``perf.jvm.fresh_jvm``:

  before: 0.25 s sleep + 1 retry x 2.0 s = 2.25 s ceiling
  after:  0 s sleep + 300 retries x 0.05 s = 15 s ceiling

Tight polling matters for XD on CodSpeed: under the old model each
XD iteration cost ~2.7 s and CodSpeed could fit only one sample per
benchmark budget, making variance and regression detection
impossible. Under the new model XD per-iteration finishes in
~500-1100 ms — enough for 2-5 samples per budget. The unconditional
0.25 s startup_sleep's original rationale ("let the OS reuse the
listen port") no longer applies: GatewayServer.startSocket() already
uses SO_REUSEADDR, so bind() succeeds immediately even over
TIME_WAIT. Backwards compatibility preserved — callers can pass
``startup_sleep=0.25`` to restore the old wait pattern.

Validated on byteoak#12, #13, #16 (where these changes
originated as 3 iterative PRs; bundled here for a clean single
upstream review surface). Full 56-cell CI matrix green on each
byteoak iteration. CodSpeed scenarios surface real cold-start work
on subsequent PRs — issue #557.

Co-authored-by: Isaac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant