Skip to content

perf(events): share one chain per handler and trigger across call sites - #7122

Merged
masenf merged 10 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/event-chain-interning
Sep 17, 2026
Merged

masenf merged 10 commits into
reflex-dev:mainfrom
FarhanAliRaza:farhan/event-chain-interning

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Second of three stacked compile-performance PRs. Stacked on #7121; this diff includes it. Merge #7121 first.

  • EventChain.create interns one chain per handler, args spec, and trigger key in RegistrationContext._bound_event_chains, keyed by (id(handler), id(args_spec), key) with the handler and spec held in the value so the ids stay valid. A handler bound to one trigger always produces the same chain, so every call site sharing the handler now shares one instance. Handlers carry no state, so deep-copying a component tree copies only the chains it contains, and a forked context starts with its own chains. Handlers with event_chain_kwargs, handlers carrying event actions (which are distinct dataclasses.replace copies), lists, lambdas, and EventSpec values are unaffected.
  • get_memoized_event_triggers keys its wrapper cache by trigger name and chain identity, holding the chain so its id cannot be recycled. The memo name stays content-hashed, so generated output is unchanged.

Why

The memoize pass rendered a fresh LiteralEventChainVar for every component to name its useCallback wrapper: ~240 µs per chain (LiteralVar.create + str + _get_all_var_data). With a hundred buttons bound to one handler that was the single largest item in the profile.

Measurements

Benchmark page with 100 rows bound to one handler (_repeated_stateful_page, full-context compile): 87.8 ms to 60.7 ms together with #7121, of which most is this change.

Docs site dry compile (511 routes, few handlers): 40.0 s (#7121) to 39.3 to 39.6 s. EventChain.create calls fell from 11.1k to 4.8k.

Micro-benchmarks, one handler bound to 8 triggers across 200 buttons, best of 5:

state EventChain.create, repeated call site copy.deepcopy of the tree build the tree
main 4301 ns 2.59 ms 4.20 ms
cache on the handler (first revision) 349 ns 1.09 ms 1.91 ms
cache on RegistrationContext (this PR) 436 ns 1.09 ms 2.01 ms

The context-owned cache costs one context-variable lookup per hit. The handler-owned revision let copy.deepcopy walk into the handler and duplicate every chain bound to it, which scaled with the number of triggers per handler; the context-owned map removes that walk.

Design note for review

Call sites that bind the same handler to the same trigger now share one EventChain object. Chains are frozen dataclasses and nothing in the framework mutates events or event_actions in place (all action helpers return replaced copies), but this is a visible change in object identity.

Test plan

  • New tests: chain interning by handler, args spec, key, kwargs, and event actions; wrapper reuse by chain identity without rendering; distinct wrappers for chains that differ in captured args or actions.
  • test_event_chain_cache_lives_on_the_registration_context: a forked context yields its own chain, the parent's chain comes back after the fork exits, and the handler retains no reference to any chain.
  • tests/units/test_event.py, tests/units/reflex_base, tests/units/components, memoize plugin tests green apart from failures that reproduce on clean main here.
  • ruff, pyright, pre-commit clean.

https://claude.ai/code/session_01PmizE1eQhtYZyVs1RK2ke3

Review in cubic

@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner September 11, 2026 23:14
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-11T23:20:30.541934Z c910356 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The implementation appears behaviorally sound, but the explicit repository requirement to extract identifier and key literals into constants must be satisfied before merging.

Findings

  1. P2 Hardcoded Trigger Identifiers

Summary

This PR improves compilation performance by interning event chains per handler, argument specification, and trigger within a registration context, then reusing memoized event wrappers by chain identity.

  • Adds context-owned caches for bound event chains and memoized trigger wrappers.
  • Clears both caches at the start of a full frontend compile.
  • Preserves distinct chains and wrappers for captured arguments, event actions, metadata, trigger names, and forked contexts.
  • Adds root and reflex-base performance news fragments plus focused unit coverage.

Reviews (7) · Last reviewed commit: "Merge branch 'main' into farhan/event-ch..."

Comment thread reflex/compiler/plugins/memoize.py

@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: c9103569b3

ℹ️ 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 reflex/compiler/plugins/memoize.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 14 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread reflex/compiler/plugins/memoize.py
Comment thread packages/reflex-base/src/reflex_base/registry.py
Comment thread packages/reflex-base/src/reflex_base/vars/base.py
Comment thread packages/reflex-base/src/reflex_base/vars/base.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/event/__init__.py
Component render, Var collection, and the prop-component scan walked every
declared prop through the field descriptor to find the few that are set.
Iterate the instance dict plus class-level defaults instead. Cache the
literal Var class per exact value type, short-circuit app-wrap dedupe on
identity, skip the generic tag protocol for plain tags, and hoist the
memoize plugin's component imports.

Docs site dry compile (511 pages): 47 s to 40 s.

Claude-Session: https://claude.ai/code/session_01PmizE1eQhtYZyVs1RK2ke3
@FarhanAliRaza
FarhanAliRaza force-pushed the farhan/event-chain-interning branch from 755f84f to aa338fc Compare September 15, 2026 13:46
EventChain.create rebuilt an identical chain for every component that bound
the same handler to the same trigger, and the memoize pass then rendered
each chain again to name its useCallback wrapper. Intern the chain on the
handler keyed by args spec and trigger, and key the wrapper cache by chain
identity so repeated call sites reuse the wrapper without rendering.

Claude-Session: https://claude.ai/code/session_01PmizE1eQhtYZyVs1RK2ke3
Deep-copying a component walked into the handler's chain cache and copied
every chain bound to it. The cache now lives on the RegistrationContext,
keyed by handler, args spec and trigger, so handlers carry no state and a
forked context starts with its own chains.
Those handlers are fresh copies at every call site, so a cached entry can
never be hit again and would only retain the copy. Also add the root news
fragment.
@FarhanAliRaza
FarhanAliRaza force-pushed the farhan/event-chain-interning branch from aa338fc to bf0d021 Compare September 15, 2026 13:49
@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 51 untouched benchmarks
⏩ 9 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_from_event_type[event_handler] 183.9 µs 191.1 µs -3.77%
test_compile_page_full_context[_stateful_page] 69.6 ms 65.6 ms +6.04%

Tip

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


Comparing FarhanAliRaza:farhan/event-chain-interning (e014e06) with main (59df268)2

Open in CodSpeed

Footnotes

  1. 9 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.

  2. No successful run was found on main (0d68f46) during the generation of this report, so 59df268 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Comment thread packages/reflex-base/src/reflex_base/components/component.py
…interning

# Conflicts:
#	packages/reflex-base/news/+compile-prop-hot-paths.performance.md
#	packages/reflex-base/src/reflex_base/components/component.py
#	reflex/compiler/plugins/memoize.py
#	tests/units/components/test_component.py
#	tests/units/components/test_tag.py
Comment thread tests/units/test_event.py
Comment thread tests/units/reflex_base/components/test_memoize_helpers.py
@masenf
masenf merged commit b760f60 into reflex-dev:main Sep 17, 2026
124 of 125 checks passed
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