Skip to content

[RSI, performance] perf(kernel): defer the event-loop import stack past the ready event - #2379

Merged
sethkarten merged 3 commits into
mainfrom
perf/kernel-defer-event-loop-imports
Sep 17, 2026
Merged

sethkarten merged 3 commits into
mainfrom
perf/kernel-defer-event-loop-imports

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Mechanism

python -X importtime on a cold python -m rlm.repl boot shows the asyncio subtree (ssl, concurrent.futures, logging ride along) is the heaviest part of the kernel import chain, and rlm.bash drags in several more stdlib modules (secrets/hmac/hashlib, shutil, datetime, selectors, struct/fcntl/termios, atexit, tempfile). All of it loaded before the ready event, so every kernel boot paid the full cost up front.

This PR moves that work off the boot path, with no protocol, API, or behavior change:

  • rlm/bash.py: binds the asyncio module global on first BashHandle construction (its only entry point — bash() is the sole constructor), and imports secrets/shutil/datetime/selectors/struct+fcntl+termios/atexit at their first real use. subprocess, socket, and _winjob stay module-level (tests patch them via rlm.bash attributes).
  • rlm/repl.py: main() sends ready first and imports asyncio afterwards, before creating the loop, reader thread, and serve task; handlers that reference asyncio (_sigint_handler, _run_guarded, _handle_state, active_cell_task, _CellExecution) import it locally. tempfile moves into _snapshot_state.
  • rlm/__init__.py needed no change on this base.

Local before/after (macOS arm64, 16 interleaved trials, harness phases replicated from scripts/benchmarks/worker.py)

Metric main this PR change
kernel_start 31.41 ms 24.42 ms -22.3%
kernel_rss 28.60 MB 26.81 MB -6.2%
kernel_exec 0.116 ms 0.117 ms +0.6% (noise)
bash 4.72 ms 4.74 ms +0.5% (noise)
git_status 13.51 ms 13.61 ms +0.7% (noise)
output 4.70 ms 4.75 ms +1.0% (noise)
mixed 123.9 ms 124.8 ms +0.8% (noise)
interrupt 0.459 ms 0.453 ms -1.3% (noise)
snapshot 7.10 ms 7.20 ms +1.4% (noise)
restore 90.1 ms 97.3 ms +8.0% (see below)
loaded_rss 73.9 MB 73.9 MB -0.0%

IQRs on kernel_start were 0.39/0.50 ms, so the startup win is far outside noise.

Honest tradeoff: the first request of a fresh kernel absorbs the one-time loop import

Total kernel work is unchanged; the asyncio import moved from boot into the first request that needs it. For cell-serving kernels the warm-up cells already cover it. For the restore benchmark (fresh kernel, restore is the first request) the restore request pays it: measured 97.8 ms vs 90.0 ms after a single warm-up cell (baseline shows no such delta, 89.6 vs 89.4 ms). Expected table movement on Linux (where imports are slower than this macOS box): restore +7-9%, which stays under the 20% comparison threshold, while kernel_start should drop well past it.

Expected benchmark movement

  • Python kernel startup: improved (largest expected win; import time on the CI box is higher than locally)
  • Python idle RSS: small improvement (asyncio/secrets/ssl not resident pre-ready), likely under the 20% threshold
  • Python state restore: +7-9% (one-time import shift), expected under the threshold
  • Cell round trip, bash, git status, 32 KiB output, mixed, interrupt, snapshot, loaded RSS: unchanged

Validation

  • uv run python -m unittest discover -s test in prime-agent-runtime: 324 tests OK (3 new: import rlm keeps asyncio/secrets off the boot path; a serving kernel loads asyncio by the first cell; BashHandle construction binds asyncio with no event loop and no prior bash() call)
  • npm run test:kernel in packages/coding-agent: 15/15 OK (real host <-> kernel round trips)
  • npm run check: OK
  • The installer/runtime RUNTIME_READY_CHECK string still passes against this tree
  • Two existing tests patched rlm.bash.secrets/rlm.bash.shutil module attributes; they now patch the stdlib modules directly (same patch semantics, since the deferred imports resolve to the same module objects)

Changelog fragment: packages/coding-agent/.changes/kernel-defer-event-loop-imports.md

Test-line budget

Fleet test-budget audit (2026-09-18): this PR exceeded the repo test-line budget gate (node scripts/check-test-policy.mjs, run by CI's Build and check job: net added test lines over changed test files may not exceed meaningful added source lines, and changed test files may not add per-category violations). The user directive driving the audit: "probably needs to remove redundant and unnecessary tests throughout. Remove unnecessary tests and we should have enough." The owner policy decision authorized executing the named-vector cut menu in cost order: "nah dont grandfather them in. we should fix them now".

  • Before: 116 net test additions vs 23 source additions (red), plus 1 wall-clock-sleep violation in prime-agent-runtime/test/test_repl.py (the SIGINT boot-window test polled a marker file with time.sleep(0.01)). CI's Build and check job is failing on exactly this budget line (run 35262369844); this head turns it green.
  • After: 22 net test additions vs 23 source additions (green), zero per-category violations (testAdded 55 / testDeleted 33). Measured at the pushed head against the merge base CI's checker uses (a7d791bc1be09793ed5f3ec05bf4cccbc60679ea), and re-confirmed with the merge-commit method against main (e2fb7bfa1): same numbers, same verdict.

What the compression did, in cost order: the two deferral vectors merged into one test; the SIGINT boot-window test was rewritten around a fifo rendezvous instead of a sleep-based marker poll (which also removed the violation); the fresh-interpreter BashHandle test was trimmed; and four small pre-existing test_repl.py vectors were folded into survivors or dropped where they were redundant. Every surviving test is honestly bounded: the SIGINT regression was re-verified by restoring the pre-fix handler ordering (install before the ready send), which makes the rewritten test fail exactly as it did before.

Lost-coverage ledger (owner-authorized cuts, per vector)

Each row records what the deleted or folded lines pinned, why it was the cheapest option per line, and any surviving partial pin. Nothing disappears silently.

  • test_serving_kernel_loads_asyncio_after_ready merged into test_import_rlm_defers_the_event_loop_stack (test_repl.py): pinned that a serving kernel has the event-loop stack resident by its first cell. Cheapest (two tests shared one subprocess/env fixture; the merge removed a def, a blank line and a duplicate comment). Surviving pins: both, unchanged asserts in the merged test.
  • test_sigint_during_boot_window_terminates_kernel rewritten as test_sigint_during_the_deferred_boot_stays_fatal (40 -> 16 lines): pinned that a SIGINT delivered while the kernel is inside the post-ready deferred import is fatal (the default handler is still in charge). Cheapest per line and the only cut that also closed a gate violation: the fake asyncio now blocks on a fifo read, and the test's blocking open of the fifo is the rendezvous, so the window is deterministic instead of polled. Dropped along with it: the reader thread/queue, the Popen + communicate plumbing, the _stop_bounded cleanup helper (ReplProcess.close already kills and waits) and the "KeyboardInterrupt" text assertion. Surviving pins: nonzero exit code after SIGINT in that window (which the regression probe confirms fails on the pre-fix ordering), and ready before the deferred import. Coverage cost: the interrupt is asserted by exit status rather than by the traceback text.
  • test_handle_construction_binds_asyncio_without_event_loop trimmed (test_bash.py, 21 -> 14 lines): pinned that a fresh interpreter with no event loop can construct and reap a BashHandle without NameError. Cheapest (comments, a temporary variable and an explicit sys.exit(0)). Surviving pins: both assertions, unchanged.
  • test_list_names_skips_non_string_keys folded into test_list_names (test_repl.py, 9 -> 1 line): pinned that non-string globals() keys are skipped by list_names. Cheapest (same request/fixture; the survivor now sets globals()[1] = 2 and asserts 1 not in names). Surviving pin: the merged assert. Coverage cost: the folded test's trailing "runtime still serves 'alive'" re-check, which every later execute in the suite covers.
  • test_stdout_buffer_write_rejects_int folded into the buffer test (test_repl.py, 16 -> 13 lines): pinned that sys.stdout.buffer.write(int) raises TypeError in the cell. Cheapest (same tagged-writer fixture, two executes in one test). Surviving pin: the TypeError + error-status asserts. Coverage cost: a redundant done status ok assert on the buffer path, which the stdout event already implies.
  • test_shutdown_clean_exit deleted (test_repl.py, 3 lines): pinned a clean shutdown exit code. Cheapest (smallest test in the file). Surviving pins: test_shutdown_after_mcp_import_exits_cleanly, test_shutdown_with_pending_host_request_exits and test_stdin_eof_with_pending_host_request_exits all assert exit code 0 on the same teardown path.
  • test_host_reply_for_unknown_id_dropped deleted (test_repl.py, 4 lines): pinned that a host_reply for an unknown id is dropped and the runtime keeps serving. Cheapest (4 lines). Surviving partial pins: test_malformed_request_line (unexpected protocol input produces an error event and the runtime still serves) and test_host_request_cancelled_cell_drops_pending_future (host-reply bookkeeping for a cancelled cell).
  • test_zero_size_cap_writes_no_empty_payload_overhead deleted (test_repl.py, 3 lines): pinned that a zero aggregate cap errors before writing and leaves no files behind. Cheapest (3 lines). Surviving partial pins: test_complete_payload_respects_aggregate_size_cap (same cap error string) and _assert_only_pair_files() in the snapshot-pair tests (no stray temp files).

Note

Medium Risk
Boot-order and SIGINT-handler timing change in the kernel entry path; behavior is intentionally preserved after serving starts but the deferred-boot window is a subtle lifecycle edge.

Overview
This PR speeds up kernel startup by keeping asyncio and related heavy stdlib imports off the path until after the host sees the ready event, without changing the JSON protocol or shell/REPL behavior once serving.

In rlm.repl, main() now emits ready first, then imports asyncio, creates the loop, starts the reader thread, and launches the serve task. The custom SIGINT handler is registered only after the loop and serve task exist, so Ctrl-C during the short post-ready boot window still uses the default handler and can terminate the process instead of being swallowed.

In rlm.bash, asyncio is no longer imported at module load; it is imported on first BashHandle construction and bound as a module global. Other previously eager imports (secrets, shutil, selectors, fcntl/struct/termios, datetime, atexit) move to the functions that first need them so import rlm stays lean before ready.

Tests add coverage for deferred imports, lazy BashHandle/asyncio binding, and SIGINT during deferred boot; existing tests patch stdlib modules directly where rlm.bash no longer re-exports them.

Reviewed by Cursor Bugbot for commit dbffdfe. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Defer kernel event-loop import stack past the ready event in rlm.repl and rlm.bash

  • Moves eager imports of asyncio, tempfile, selectors, secrets, shutil, fcntl, termios, struct, datetime, and atexit out of module initialization in bash.py and repl.py; each is now imported at first use
  • Sends the kernel ready event in rlm.repl.main before importing asyncio and creating the event loop, request reader thread, and serve task
  • Updates tests to patch secrets.token_hex and shutil.which at their stdlib module locations and adds subprocess tests verifying rlm import excludes the event-loop stack
  • Behavioral Change: SIGINT received between the ready event and installation of _sigint_handler in repl.py is handled by the process default (fatal) rather than the custom handler, which is now installed only after the serve task exists

Macroscope summarized dbffdfe.

The Python kernel imported asyncio (plus the bash tool's secrets, shutil,
datetime, selectors, struct, fcntl/termios, atexit, and tempfile) before
sending the ready event. python -X importtime shows the asyncio subtree is
the heaviest part of the boot chain (ssl, concurrent.futures, and logging
ride along).

Defer them: rlm.bash binds asyncio on first BashHandle construction (its
only entry point), repl imports asyncio in main() after the ready event and
in the handlers that reference it, and the remaining stdlib modules import
at their first real use. No protocol, API, or behavior changes; the kernel
serves requests and interrupts exactly as before.

Local A/B (macOS, 16 interleaved trials, same harness phases as
scripts/benchmarks):
- kernel_start: 31.4 -> 24.4 ms (-22.3%)
- kernel_rss: 28.6 -> 26.8 MB (-6.2%)
- kernel_exec, bash, git_status, output, mixed, interrupt, snapshot,
  loaded_rss: unchanged within noise
- restore: 90.1 -> 97.3 ms (+8%): the one-time loop import now lands in
  the first request of a fresh kernel instead of its boot; after one
  warm-up cell restore returns to parity (90.0 vs 89.4 ms baseline)

Regression tests: import rlm must keep asyncio/secrets off the boot path;
a serving kernel loads asyncio by the first cell; BashHandle construction
binds asyncio with no event loop or prior bash() call.
@sethkarten
sethkarten enabled auto-merge (squash) September 16, 2026 00:51
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

Prime Agent performance — partial

PR dbffdfeb compared with main c13e0311.

Benchmark execution did not complete successfully. Missing measurements are not performance wins.

Failure diagnostics:

  • Benchmark claimed completion with failed or missing measurements

See the saved per-trial logs and terminal transcripts for details.

Overall: 0 regressed · 25 improved · 15 no clear change · 1 unavailable.

Metric Main This PR Change
Cold startup 1,784.6 ms 1,132.7 ms $\textcolor{#4c8762}{\textsf{↓ -651.9 ms (-36.53\%)}}$
Warm startup 1,068.0 ms 743.5 ms $\textcolor{#508664}{\textsf{↓ -324.4 ms (-30.38\%)}}$
Installation 15.31 s 12.54 s ≈ -2.78 s (-18.13%)
Compressed release artifacts 72.51 MB 71.11 MB $\textcolor{#65816c}{\textsf{↓ -1.40 MB (-1.93\%)}}$
Installed footprint 577.65 MB 574.24 MB ≈ -3.41 MB (-0.59%)
Idle memory, summed RSS 1,282.00 MB 1,200.64 MB ≈ -81.36 MB (-6.35%)

Python runtime

Metric Main This PR Change
Python kernel startup 149.4 ms 85.4 ms $\textcolor{#488860}{\textsf{↓ -64.0 ms (-42.83\%)}}$
Python cell round trip 0.629 ms 0.447 ms $\textcolor{#518664}{\textsf{↓ -0.182 ms (-28.96\%)}}$
Empty bash command 12.7 ms 10.0 ms $\textcolor{#578566}{\textsf{↓ -2.7 ms (-20.98\%)}}$
Bash git status 19.4 ms 14.0 ms $\textcolor{#528664}{\textsf{↓ -5.4 ms (-27.68\%)}}$
Bash 32 KiB output 13.0 ms 9.8 ms $\textcolor{#558565}{\textsf{↓ -3.2 ms (-24.34\%)}}$
35 cells / 9 shell calls 184.7 ms 142.0 ms $\textcolor{#568566}{\textsf{↓ -42.8 ms (-23.15\%)}}$
Python interrupt to done 1.541 ms 1.336 ms ≈ -0.204 ms (-13.26%)
Python state snapshot 28.3 ms 22.9 ms ≈ -5.4 ms (-19.06%)
Python state restore 398.9 ms 324.6 ms ≈ -74.3 ms (-18.62%)
Python idle RSS 35.97 MB 28.17 MB $\textcolor{#578566}{\textsf{↓ -7.80 MB (-21.68\%)}}$
Python RSS after pandas workload 97.94 MB 96.86 MB ≈ -1.07 MB (-1.10%)

Session transport

Metric Main This PR Change
Private frame decode, 32 MiB in 8 KiB chunks

UI interactions

Metric Main This PR Change
Resume large session (cold) 4,185.5 ms 2,820.2 ms $\textcolor{#4f8763}{\textsf{↓ -1,365.3 ms (-32.62\%)}}$
CPU, resume large session 7,180.0 ms 4,370.0 ms $\textcolor{#4a8861}{\textsf{↓ -2,810.0 ms (-39.14\%)}}$
Switch into large session 5,812.5 ms 3,754.5 ms $\textcolor{#4d8762}{\textsf{↓ -2,058.0 ms (-35.41\%)}}$
CPU, switch into large session 10,780.0 ms 7,150.0 ms $\textcolor{#4e8763}{\textsf{↓ -3,630.0 ms (-33.67\%)}}$
Open agents view from a session 177.0 ms 146.3 ms ≈ -30.7 ms (-17.33%)
CPU, open agents view 470.0 ms 430.0 ms ≈ -40.0 ms (-8.51%)
Full agents roster, many sessions 4.47 s 4.44 s ≈ -0.02 s (-0.50%)
CPU, full agents roster 3.02 s 2.07 s ≈ -0.95 s (-31.46%)
Open another session from agents view 2,818.0 ms 2,317.2 ms ≈ -500.8 ms (-17.77%)
CPU, open from agents view 3,230.0 ms 2,180.0 ms $\textcolor{#4f8763}{\textsf{↓ -1,050.0 ms (-32.51\%)}}$
Reopen resident large session 653.1 ms 479.5 ms $\textcolor{#538665}{\textsf{↓ -173.7 ms (-26.59\%)}}$
CPU, reopen resident session 1,140.0 ms 890.0 ms $\textcolor{#568566}{\textsf{↓ -250.0 ms (-21.93\%)}}$
Open subagent session at depth 6 21,638.7 ms 20,155.1 ms ≈ -1,483.6 ms (-6.86%)
CPU, open subagent at depth 6 11,250.0 ms 8,230.0 ms $\textcolor{#538665}{\textsf{↓ -3,020.0 ms (-26.84\%)}}$
Open chain parent from agents view 4,235.2 ms 3,427.7 ms ≈ -807.5 ms (-19.07%)
CPU, open chain parent 4,370.0 ms 2,970.0 ms $\textcolor{#4f8663}{\textsf{↓ -1,400.0 ms (-32.04\%)}}$
Scheduled catalog, first request 1,961.4 ms 1,300.8 ms $\textcolor{#4e8763}{\textsf{↓ -660.6 ms (-33.68\%)}}$
CPU, scheduled catalog 3,350.0 ms 2,250.0 ms $\textcolor{#4f8763}{\textsf{↓ -1,100.0 ms (-32.84\%)}}$
Scheduled catalog, repeated request 1,260.0 ms 789.7 ms $\textcolor{#4c8761}{\textsf{↓ -470.3 ms (-37.32\%)}}$
CPU, repeated catalog 1,680.0 ms 1,130.0 ms $\textcolor{#4f8763}{\textsf{↓ -550.0 ms (-32.74\%)}}$
Cold worker with three catalog scans 1,629.3 ms 1,124.3 ms $\textcolor{#508663}{\textsf{↓ -505.0 ms (-31.00\%)}}$
CPU, cold worker and scans 4,500.0 ms 3,350.0 ms $\textcolor{#548565}{\textsf{↓ -1,150.0 ms (-25.56\%)}}$
UI memory after interactions 2,721.03 MB 2,571.51 MB ≈ -149.53 MB (-5.50%)

Sandbox cost: ~$0.1400 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-17T21:06:23.069826+00:00. Harness c13e0311.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Transport benches run node against the prepared source build, outside the installed home.
Frame decode times one 32 MiB private frame, snapshot-chunk header, pushed in
8 KiB chunks; the wire shape of multi-MB frames on the daemon-worker channels.
UI trials use a fresh fixture set: 194 top-level sessions including one ~40 MB transcript,
40 ledger fan-out children, and a 6-deep subagent chain (~46 spawn edges).
Large fixtures hold 1,999 complete triples (~5 MB JSONL); medium 119; subagents 399 each.
Interactions: cold --resume of a large session, warm /resume switch, left-arrow to agents view,
roster settle with many saved sessions, search-and-open of another large session,
reattaching to that resident session, opening the chain parent, and drilling to depth 6.
Readiness is the rendered transcript tail plus a confirmed editor echo.
CPU metrics sum utime+stime across the whole benchmark-user process tree per interaction.
UI memory sums RSS after the interactions; PTY byte counts are in the raw results.
A separate catalog fixture has 2,300 sessions, 2,298 edges, and 13 paused scheduled-job owners.
Catalog timings cover first/repeated reads and cold worker creation under three pending scans.
All expected jobs and owner metadata are checked; worker readiness excludes TUI rendering.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Performance changes are informational.
Failed or incomplete execution fails the workflow; saved artifacts remain available.
Each side stops a phase after 2 identical consecutive failures.
Skipped trials are not attempted samples. Warm startup requires a successful cold launch.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 118.1 ms IQR 134.8 ms
Warm startup 10/10 10/10 IQR 209.5 ms IQR 91.8 ms
Installation 3/3 3/3 range 0.92 s range 1.44 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 12.96 MB IQR 20.59 MB
Python kernel startup 10/10 10/10 IQR 26.5 ms IQR 13.8 ms
Python cell round trip 10/10 10/10 IQR 0.081 ms IQR 0.059 ms
Empty bash command 10/10 10/10 IQR 2.4 ms IQR 0.9 ms
Bash git status 10/10 10/10 IQR 1.0 ms IQR 2.0 ms
Bash 32 KiB output 10/10 10/10 IQR 1.7 ms IQR 2.2 ms
35 cells / 9 shell calls 10/10 10/10 IQR 11.4 ms IQR 10.5 ms
Python interrupt to done 10/10 10/10 IQR 0.271 ms IQR 0.461 ms
Python state snapshot 10/10 10/10 IQR 7.6 ms IQR 5.6 ms
Python state restore 10/10 10/10 IQR 50.3 ms IQR 23.4 ms
Python idle RSS 10/10 10/10 IQR 1.26 MB IQR 4.19 MB
Python RSS after pandas workload 10/10 10/10 IQR 1.83 MB IQR 4.20 MB
Private frame decode, 32 MiB in 8 KiB chunks 0/0 0/0
Resume large session (cold) 3/3 3/3 range 405.8 ms range 847.2 ms
CPU, resume large session 3/3 3/3 range 1,550.0 ms range 690.0 ms
Switch into large session 3/3 3/3 range 944.2 ms range 404.8 ms
CPU, switch into large session 3/3 3/3 range 1,470.0 ms range 1,390.0 ms
Open agents view from a session 3/3 3/3 range 37.5 ms range 17.5 ms
CPU, open agents view 3/3 3/3 range 120.0 ms range 210.0 ms
Full agents roster, many sessions 3/3 3/3 range 0.41 s range 0.40 s
CPU, full agents roster 3/3 3/3 range 1.34 s range 0.04 s
Open another session from agents view 3/3 3/3 range 298.6 ms range 160.1 ms
CPU, open from agents view 3/3 3/3 range 270.0 ms range 270.0 ms
Reopen resident large session 3/3 3/3 range 27.9 ms range 84.9 ms
CPU, reopen resident session 3/3 3/3 range 140.0 ms range 190.0 ms
Open subagent session at depth 6 3/3 3/3 range 589.0 ms range 926.2 ms
CPU, open subagent at depth 6 3/3 3/3 range 1,080.0 ms range 1,090.0 ms
Open chain parent from agents view 3/3 3/3 range 353.5 ms range 135.2 ms
CPU, open chain parent 3/3 3/3 range 1,240.0 ms range 250.0 ms
Scheduled catalog, first request 3/3 3/3 range 116.6 ms range 85.8 ms
CPU, scheduled catalog 3/3 3/3 range 170.0 ms range 270.0 ms
Scheduled catalog, repeated request 3/3 3/3 range 117.9 ms range 124.5 ms
CPU, repeated catalog 3/3 3/3 range 180.0 ms range 240.0 ms
Cold worker with three catalog scans 3/3 3/3 range 27.8 ms range 199.9 ms
CPU, cold worker and scans 3/3 3/3 range 420.0 ms range 700.0 ms
UI memory after interactions 3/3 3/3 range 70.33 MB range 89.83 MB

Failures:

  • Benchmark claimed completion with failed or missing measurements

Comment thread prime-agent-runtime/src/rlm/repl.py Outdated
@sethkarten sethkarten changed the title [RSI] perf(kernel): defer the event-loop import stack past the ready event [RSI, performance] perf(kernel): defer the event-loop import stack past the ready event Sep 16, 2026
…boot

_sigint_handler has no task to target before serving starts, so the PR-head
ordering (install before the ready send) silently swallowed a Ctrl-C during
the deferred asyncio import and event-loop startup. Keep the default handler
until the loop and _serve_task exist, then install the custom handler.

Adds a regression test that parks the kernel inside the post-ready deferred
import (fake asyncio on PYTHONPATH) and asserts a SIGINT there terminates
the kernel; it fails on the pre-fix ordering and passes with the fix.
@sethkarten sethkarten closed this Sep 17, 2026
auto-merge was automatically disabled September 17, 2026 07:01

Pull request was closed

@sethkarten
sethkarten deleted the perf/kernel-defer-event-loop-imports branch September 17, 2026 07:01
@sethkarten sethkarten reopened this Sep 17, 2026
@sethkarten
sethkarten restored the perf/kernel-defer-event-loop-imports branch September 17, 2026 19:01
Net test additions drop 116 -> 22 against the 23 meaningful source lines the
deferred event-loop import adds, so the test-policy gate passes at branch level.

Cuts: merge the import-deferral and serving vectors; park the fake asyncio on a
fifo read instead of polling for a marker with time.sleep (this also removes the
wall-clock-sleep violation); trim the fresh-interpreter handle test; fold the
no-string-key list_names vector into test_list_names; fold the int-reject vector
into the stdout.buffer test; drop the redundant clean-shutdown, unknown-id
host_reply and zero-size-cap tests. Per-cut ledger is in the PR body.
@kevinjosethomas

Copy link
Copy Markdown
Member

Second-pass review findings — holding:

  • Test-line-budget gate fails (Build and check / check:test-policy): net test additions 116 vs source additions 23. All test suites themselves pass — only the budget gate is red.
  • The new prime-agent-runtime/test/test_repl.py harness is trimmable without losing coverage: the serving-asyncio check overlaps existing execution tests, and the SIGINT case duplicates subprocess/queue/release machinery — a fake asyncio module that sends itself SIGINT can replace the marker/release polling. Cold-import, standalone-handle, and deterministic boot-SIGINT coverage should be kept.
  • Claim wording: ready-before-asyncio-import is boot-order deferral, not demand-driven loading (scripts/benchmarks/worker.py measures ready + immediate RSS, before warmups). The win is earlier handshake + transient RSS, not first-cell latency or steady idle. Also the new post-ready default-SIGINT window means "no behavior changes" is slightly overbroad.
  • Lazy-import audit itself is sound: rlm/bash.py binds asyncio before handle paths, REPL runtime references import locally, and test patch targets (subprocess/socket/_winjob, shared stdlib shutil/secrets) remain available.

@sethkarten
sethkarten enabled auto-merge (squash) September 17, 2026 22:38
@sethkarten
sethkarten merged commit 8218d6b into main Sep 17, 2026
86 of 88 checks passed
@sethkarten
sethkarten deleted the perf/kernel-defer-event-loop-imports branch September 17, 2026 23:43
sethkarten added a commit that referenced this pull request Sep 18, 2026
main carries the boot-lean import change and the async-bash notice-race fix;
this branch carries the recursive chmod/chown escape guard. Only the
module-level import block conflicted, resolved the same way as the sibling
guard branches: keep main's deferred layout, keep the guard's cheap module-scope
needs (`re` for its compiled patterns, `Collection` and dataclasses `replace`
from modules main already imports), and drop the guard's module-level
`secrets`/`selectors`/`shutil`/`datetime`/`timezone` so #2379's deferral pin
(`import rlm` loads no asyncio/secrets) still holds. Guard functions that need
the deferred modules import them locally, matching the established pattern.

Verified: full runtime suite 425 tests OK; deferral test ok.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants