Skip to content

ENG-10164 Add event-loop performance benchmark suite - #6751

Open
Alek99 wants to merge 13 commits into
claude/reflex-compiler-perf-t8ztc9-15-snapshot-memo-walksfrom
agent/event-loop-performance-benchmarks
Open

ENG-10164 Add event-loop performance benchmark suite#6751
Alek99 wants to merge 13 commits into
claude/reflex-compiler-perf-t8ztc9-15-snapshot-memo-walksfrom
agent/event-loop-performance-benchmarks

Conversation

@Alek99

@Alek99 Alek99 commented Jul 13, 2026

Copy link
Copy Markdown
Member

What changed

  • add a documented, owned performance-testing structure with deterministic CodSpeed benchmarks and opt-in wall-time suites
  • expand event processing into cold/warm, same-token, independent-token, and burst scenarios
  • benchmark event-loop queueing, task lifecycle, streaming updates, payloads, serialization, state deltas, state managers, routes, headers, proxies, and Var operations
  • add wall-time event-loop attribution with loop-lag percentiles, queue/task gauges, 80/20 hot-token load, handler-shape coverage, ordering checks, trace output, and failure diagnostics
  • add manual real-Redis latency/contention, production Socket.IO load, memory retention, compiler/hot-reload lifecycle, and browser/bundle suites
  • run event-loop and browser smoke coverage in the existing PR performance workflow

Review fixes

  • make loop-probe reset coverage independent of platform timer resolution
  • preserve every repeated pipeline-stage pair for streaming handlers
  • keep the event loop's default executor usable after load-client runs
  • exercise the real BaseStateToken hierarchy and persisted warm-state path in memory and Redis benchmarks
  • measure hot reload through one persistent reflex run --backend-only watcher and an observable live source-version endpoint

Review fixes (round 2)

  • drop the out-of-scope nightly workflow, comparison script, and PERFORMANCE.md
  • guard the Redis helper: REFLEX_PERF_REDIS_URL must name an explicit db index and the database must be empty; cleanup is exception-safe
  • measure the bundle budget against the shipped client bundle (.web/build/client, measured 238 KB gzip) instead of all of .web; 500 KB budget overridable via REFLEX_PERFORMANCE_JS_GZIP_BUDGET
  • bound all PR-smoke event-future waits with scale-aware timeouts and give the benchmarks CI job a timeout-minutes
  • benchmark real product code: reflex.app._decode_asgi_headers (extracted, behavior identical) instead of a local copy, and re.purge() per round so route-table construction is actually cold
  • move processor lifecycle out of measured regions, assert shutdown drain actually drains, and raise the drain budget so instrumentation slowdown cannot flip drain into cancel
  • de-flake the wall-time suites: token-cleanup polling instead of a fixed sleep, concurrency-scaled client timeouts with one retry, holder-process kill fallback, untraced warmup cycles before the leak gate, dev-backend child sweep + port-race retry
  • move the browser smoke into its own CI job and upload performance smoke artifacts from both jobs

Benchmark model

tests/benchmarks/ contains deterministic CPU work suitable for CodSpeed instrumentation. tests/performance/ contains wall-time, service, process, memory, compiler, load, and browser scenarios; it is skipped unless --run-performance is supplied.

Shared schema-v1 reporting, environment metadata, percentiles, loop probes, pipeline traces, state/app fixtures, Redis isolation, Socket.IO clients, and diagnostics live under tests/benchmarks/support/.

CI scope

This draft adds pull-request smoke coverage to the existing performance workflow, plus artifact upload of the smoke reports. Per scope decision, it intentionally excludes all other former step-4 work: nightly or release workflows, automated profiler collection, runner validation, comparison tooling, and standalone external-load tooling.

Validation

  • uv run pytest tests/benchmarks --codspeed -q — 88 passed
  • uv run pytest tests/units -q — 6645 passed, 78.04% coverage
  • uv run pytest tests/performance -q — skipped by default; event-loop and memory smoke ran for real (2 passed), plus a real production export to measure the shipped bundle
  • Ruff format/check passed; Pyright passed with 0 errors; workflow YAML validated

Review notes

The wall-time suites emit measurements without performance thresholds; regression gating should only be enabled after enough comparable baseline runs establish variance. Two narrow correctness gates are intentional: the shipped-bundle gzip budget (2x measured headroom, env-overridable) and the loop-probe detection bound (2 ms against a deliberately induced 10 ms block).

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a broad performance benchmark suite for the event loop and related framework paths. The main changes are:

  • Deterministic benchmarks and opt-in wall-time suites.
  • Shared reporting, diagnostics, tracing, fixtures, and service helpers.
  • Event-loop, state, Redis, compiler, browser, bundle, and load coverage.
  • Pull-request smoke jobs with bounded runtime and artifact uploads.
  • Runtime performance improvements across event processing, state handling, memoization, routing, and Vars.

Confidence Score: 5/5

This looks safe to merge.

  • Terminal event-loop blocking is recorded before sampler cancellation.
  • The sampler is excluded from reported task counts.
  • Focused tests cover both fixes and probe reuse.

Important Files Changed

Filename Overview
tests/benchmarks/support/loop_probe.py Records terminal scheduling delay before shutdown and excludes the sampler from live-task measurements.
tests/units/benchmarks/test_loop_probe.py Covers terminal blocking, sampler exclusion, and probe reset behavior.
.github/workflows/performance.yml Adds bounded event-loop and browser smoke jobs with artifact uploads.

Reviews (15): Last reviewed commit: "Fix more benchmark accuracy findings: re..." | Re-trigger Greptile

Comment thread tests/benchmarks/support/loop_probe.py
Comment thread tests/benchmarks/loop_probe.py Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 70.6%

❌ 1 regressed benchmark
✅ 26 untouched benchmarks
🆕 62 new benchmarks
⏩ 8 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_process_event 7.2 ms 24.5 ms -70.6%
🆕 test_process_event_cold N/A 20.2 ms N/A
🆕 test_process_event_warm N/A 2.7 ms N/A
🆕 test_emit_update N/A 146.8 µs N/A
🆕 test_event_future_tree N/A 513.6 µs N/A
🆕 test_event_queue_dispatch N/A 368.2 µs N/A
🆕 test_event_shutdown_cancel N/A 618.1 µs N/A
🆕 test_event_shutdown_drain N/A 1.9 ms N/A
🆕 test_yield_intermediate_update N/A 659.3 µs N/A
🆕 test_event_payload_mutable_proxy N/A 7.2 ms N/A
🆕 test_event_payload_plain_collection N/A 29.9 ms N/A
🆕 test_process_event_burst_independent_tokens[1] N/A 2.7 ms N/A
🆕 test_process_event_burst_independent_tokens[10] N/A 21.5 ms N/A
🆕 test_process_event_burst_independent_tokens[100] N/A 210.3 ms N/A
🆕 test_process_event_burst_same_token[1] N/A 2.7 ms N/A
🆕 test_process_event_burst_same_token[10] N/A 27.8 ms N/A
🆕 test_process_event_burst_same_token[100] N/A 962.6 ms N/A
🆕 test_route_argument_extraction N/A 58.3 µs N/A
🆕 test_route_matching[10] N/A 26.1 µs N/A
🆕 test_route_matching[100] N/A 24.8 µs N/A
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing agent/event-loop-performance-benchmarks (fbf0b27) with claude/reflex-compiler-perf-t8ztc9-15-snapshot-memo-walks (86c42bd)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Alek99 Alek99 changed the title Add event loop performance benchmarks Add event-loop performance benchmark suite Jul 13, 2026
@FarhanAliRaza
FarhanAliRaza marked this pull request as ready for review July 13, 2026 16:26
@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner July 13, 2026 16:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d88639608

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/benchmarks/support/events.py Outdated
@Alek99 Alek99 added the skip-changelog For doc/internal changes label Jul 13, 2026
@FarhanAliRaza FarhanAliRaza changed the title Add event-loop performance benchmark suite ENG-10164 Add event-loop performance benchmark suite Jul 13, 2026
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

ENG-10164

Alek99 and others added 13 commits July 17, 2026 18:44
…nchmarks

Close the measurement gaps identified against peer frameworks
(Phoenix LiveView, Blazor Server, Streamlit, Gradio, krausest):

- wire-size suite recording serialized delta bytes per canonical
  interaction, plus received payload bytes in the load client
- framework-overhead baseline against a bare Starlette + python-socketio
  echo server with byte-matched responses
- latency-throughput curve with saturation-knee detection in event load
- reconnect-storm scenario (simultaneous reconnect after all clients drop)
- per-session backend memory with clients held in a subprocess
- keyed-list browser ops (create/partial-update/select/swap) timed
  through paint on a new /rows page
- compare_performance_results.py for baseline-vs-current regression
  gating with percentage thresholds and absolute floors
- nightly release-scale workflow with a rolling cached baseline

The production app harness is now a session fixture shared by the load,
memory, and browser suites to avoid repeated production builds.
- guard the perf Redis helper: require an explicit db index in
  REFLEX_PERF_REDIS_URL and refuse to run against a non-empty database;
  make cleanup exception-safe and flush before close
- measure the bundle budget against the shipped client bundle
  (.web/build/client) instead of all of .web; budget 500KB gzip (~2x a
  measured 238KB export), overridable via REFLEX_PERFORMANCE_JS_GZIP_BUDGET
- bound all event-future waits in the PR-smoke event-loop suite with
  scale-aware timeouts; add timeout-minutes to the benchmarks CI job
- benchmark the real header-decode path via reflex.app._decode_asgi_headers
  instead of a local copy; re.purge() per round so route-table construction
  is actually cold; drop asyncio.get_event_loop() from sync benchmarks
- move processor construction/lifecycle out of measured regions in the
  event-loop benchmarks; assert shutdown drain actually drains; raise the
  drain budget so instrumentation slowdown cannot flip drain to cancel
- replace the reconnect-storm settling sleep with token-cleanup polling,
  scale client timeouts with concurrency, and retry transient storm
  failures once before asserting
- kill and report the session holder on wait timeout; add untraced warmup
  cycles before the memory leak gate; sweep pre-collected dev-backend
  children on teardown and retry lost port races
- format each async stack frame once in diagnostics; integer Chrome trace
  tids with thread_name metadata; surface baseline-server thread crashes;
  use the public api_transformer seam; add tests/units/benchmarks/__init__.py
- move the browser smoke into its own CI job and upload performance smoke
  artifacts
…asserts

Address review findings that several benchmarks did not measure what they
claim:

- socket load client: prime + drain the hydrate/on_load deltas a fresh token
  emits so measured events are 1:1 request/response instead of pipelined two
  deep, fixing the event-load latency curve and framework-overhead comparison
  (which could otherwise report negative overhead).
- browser hydration: gate on the websocket-driven is_hydrated marker instead of
  the prerendered #count text, so the timing spans real hydration; add the
  marker to the load app's index page.
- route matching: seed a cached rxconfig so get_config() is a dict lookup as in
  production, instead of rebuilding Config (sys.path scan + env parse) on every
  match, which was 55-97% of the measured region.
- process_event: purge the shared token per pedantic round so the cold path is
  actually exercised; CodSpeed's warmup otherwise left every round all-warm.
- event-loop leak asserts: capture orphan task/future counts before stop()
  (which clears them), so the checks and metrics can catch a real leak; wait for
  the backend-exception task to drain in the failed-pipeline case.
- wire size: encode with socket.io's compact separators so wire_bytes equals the
  real payload size instead of overstating it ~20%.
- redis helper: close the manager before flushing (via a fresh client) so oplock
  lease write-backs land before the wipe and the next run's empty-db guard holds.
- compiler lifecycle: keep zipping on the backend export so it measures real
  work rather than a no-op.
…th, ordering

Second review pass:

- state-manager cold get: run the measured construction inside an isolated
  RegistrationContext registering only PerformanceState, so it instantiates that
  state's subtree instead of every state in the collected session. Adding or
  removing an unrelated benchmark state no longer shifts this CodSpeed result.
- event queue dispatch / future tree: run the measured dispatch inside the same
  context where start() attached the EventContext, so enqueue takes the
  production EventContext.get().fork() path instead of the LookupError fallback;
  build the event in per-round setup so only queueing and dispatch are measured.
- wall-time ordering scenario: track handlers active per token and assert the
  peak stays 1. Completion-order alone can't catch a regression to concurrent
  same-token dispatch (equal sleeps still finish in order); the overlap counter
  can, and it's keyed per token so independent tokens still run concurrently.
- compiler lifecycle: disable the live PyPI latest-version check so `init`
  timing is not contaminated by a network round-trip.
@Alek99
Alek99 force-pushed the agent/event-loop-performance-benchmarks branch from 8745f9e to fbf0b27 Compare July 18, 2026 01:46
@Alek99
Alek99 changed the base branch from main to claude/reflex-compiler-perf-t8ztc9-15-snapshot-memo-walks July 18, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog For doc/internal changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants