Skip to content

Fast-path framework bookkeeping fields in state attribute access - #6757

Open
Alek99 wants to merge 4 commits into
agent/event-loop-performance-benchmarksfrom
claude/reflex-perf-optimizations-01l7a3-eng-10096
Open

Fast-path framework bookkeeping fields in state attribute access#6757
Alek99 wants to merge 4 commits into
agent/event-loop-performance-benchmarksfrom
claude/reflex-perf-optimizations-01l7a3-eng-10096

Conversation

@Alek99

@Alek99 Alek99 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Linear: ENG-10096

Description

_get_attribute was ~43% of profiled event-handling time, and much of that was the framework's own reads of dirty_vars, parent_state, substates, and _backend_vars paying the full inherited-vars/event-handler/proxying chain on every access from _mark_dirty/_clean/get_delta.

  • Add those names (plus dirty_substates) to the CLASS_VAR_NAMES fast path. They are never state vars, never inherited, and were never proxied (not in base_vars/backend_vars), so resolution via object.__getattribute__ is behavior-identical; assignments already ended in object.__setattr__ with no dirty-marking.
  • Reorder the proxying condition so base_vars/backend_vars membership is checked before is_mutable_type, sparing non-var values the type check.
  • Cache get_skip_vars() per class as a frozenset, invalidated when inherited_vars changes (_update_substate_inherited_vars, add_var).
  • Cache the frontend computed var names used by get_delta per class, invalidated when computed_vars changes (_evaluate, dynamic route vars).

Benchmarks (GitHub Actions runner, run, 2 passes each)

Case main this PR speedup
4 bookkeeping attr reads 3.66 / 3.56 us 0.61 / 0.66 us ~5.7x
setattr + get_delta + _clean cycle 84.4 / 71.4 us 54.5 / 51.4 us ~1.4x
get_skip_vars() 0.49 / 0.48 us 0.15 / 0.15 us ~3.3x

cProfile (5000 bookkeeping reads + 500 event cycles): _get_attribute tottime drops 0.120s -> 0.061s; total function calls drop 256k -> 207k.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
    • test_get_skip_vars_cached_per_class: per-class caching with inherited vars included and no cross-class leakage.
    • test_frontend_computed_var_names_cached_per_class: backend computed vars excluded; cached identity.
    • test_framework_bookkeeping_fields_not_proxied: bookkeeping containers stay raw while real state vars still get proxied.
  • Have you successfully ran tests with your changes locally?

🤖 Generated with Claude Code

https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X


Generated by Claude Code

@Alek99
Alek99 requested a review from a team as a code owner July 14, 2026 00:35
@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

ENG-10096

@codspeed-hq

codspeed-hq Bot commented Jul 14, 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

⚡ 33 improved benchmarks
❌ 1 regressed benchmark
✅ 55 untouched benchmarks
⏩ 8 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_var_access[non_mutable_scalar] 58.5 ms 66 ms -11.31%
Simulation test_state_tree_delta[depth_10] 2.8 ms 1.9 ms +45.83%
Simulation test_proxy_mapping_mutation 6.4 ms 4.4 ms +44.55%
Simulation test_computed_dependency_fanout[0] 364.2 µs 253.7 µs +43.55%
Simulation test_state_tree_delta[three_by_three] 2.5 ms 1.8 ms +40.43%
Simulation test_state_tree_delta[width_10] 2.1 ms 1.5 ms +37.67%
Simulation test_dirty_computed_var_propagation[100] 8.3 ms 6.2 ms +33.21%
Simulation test_proxy_list_mutation 7.8 ms 5.9 ms +33.17%
Simulation test_dirty_computed_var_propagation[10] 1,233.9 µs 955.9 µs +29.08%
Simulation test_computed_dependency_fanout[2] 602.6 µs 481.6 µs +25.13%
Simulation test_state_delta_scalar_mutation[10] 659.1 µs 535 µs +23.19%
Simulation test_dirty_computed_var_propagation[1] 535.3 µs 440.2 µs +21.61%
Simulation test_state_delta_scalar_mutation[100] 730.8 µs 602.9 µs +21.22%
Simulation test_state_manager_memory_cold_get 430.1 µs 358.3 µs +20.05%
Simulation test_process_event_burst_independent_tokens[100] 212.5 ms 181.5 ms +17.07%
Simulation test_process_event_burst_independent_tokens[10] 21.7 ms 18.6 ms +16.62%
Simulation test_computed_dependency_fanout[10] 1.5 ms 1.3 ms +13.71%
Simulation test_process_event_burst_independent_tokens[1] 2.7 ms 2.4 ms +13.27%
Simulation test_process_event_warm 2.7 ms 2.4 ms +13.14%
Simulation test_process_event_burst_same_token[1] 2.7 ms 2.4 ms +13.13%
... ... ... ... ... ...

ℹ️ 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 claude/reflex-perf-optimizations-01l7a3-eng-10096 (932c1fd) with agent/event-loop-performance-benchmarks (b938b34)

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.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR speeds up state attribute access and repeated state serialization work. The main changes are:

  • Direct access for framework bookkeeping fields.
  • Per-class caching of serialization skip names.
  • Per-class caching of frontend computed-variable names.
  • Cache invalidation when inherited or computed variables change.
  • Tests for cache isolation and mutable state proxying.

Confidence Score: 5/5

No additional blocking issue qualifies for this follow-up review.

  • No distinct production failure remains to report from the reviewed changes.

Important Files Changed

Filename Overview
reflex/state.py Adds bookkeeping attribute fast paths, cached state metadata, and invalidation hooks.
tests/units/test_state.py Adds tests for per-class caches, computed-variable filtering, and bookkeeping field access.
news/6757.performance.md Documents the state attribute-access and serialization performance improvements.

Reviews (5): Last reviewed commit: "Trigger CI on rebased branch" | Re-trigger Greptile

Comment thread reflex/state.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: 483d2d1253

ℹ️ 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/state.py
for substate_class in cls.get_substates():
substate_class.vars.setdefault(name, var)
# inherited_vars may alias this class's vars dict.
substate_class._skip_var_names = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recursively invalidate skip-var caches for dynamic vars

When add_var() is called on a state that already has nested substates, this clears the cache only for immediate children. Grandchildren have their inherited_vars updated indirectly because it aliases the child vars dict, but their _skip_var_names frozenset was already populated during class creation and remains stale, so Grandchild.get_skip_vars() omits the newly inherited dynamic var. Please invalidate descendants as well when propagating a dynamically added var.

Useful? React with 👍 / 👎.

@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10096 branch from 97e9e37 to 1ded8bd Compare July 18, 2026 01:46
@Alek99
Alek99 changed the base branch from main to agent/event-loop-performance-benchmarks July 18, 2026 01:46
@Alek99
Alek99 force-pushed the agent/event-loop-performance-benchmarks branch from fbf0b27 to 15bdf67 Compare August 12, 2026 23:32
@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10096 branch from 1ded8bd to b8dc9e0 Compare August 12, 2026 23:32
claude added 3 commits August 12, 2026 17:43
_get_attribute was 43% of profiled event-handling time; much of it was
the framework's own reads of dirty_vars, parent_state, substates, and
_backend_vars paying the full inherited-vars/event-handler/proxying
chain on every access.

- Add those names (and dirty_substates) to the CLASS_VAR_NAMES fast
  path; they are never state vars, never inherited, and never proxied.
- Check base_vars/backend_vars membership before is_mutable_type when
  deciding whether to proxy, so non-var values skip the type check.
- Cache get_skip_vars() per class (invalidated when inherited_vars
  changes) instead of rebuilding the set on every internal write.
- Cache the frontend computed var names used by get_delta per class
  (invalidated when computed_vars changes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
@Alek99
Alek99 force-pushed the agent/event-loop-performance-benchmarks branch from 15bdf67 to b938b34 Compare August 13, 2026 00:44
@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10096 branch from b8dc9e0 to 88919b5 Compare August 13, 2026 00:44
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X

@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.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/units/test_state.py">

<violation number="1" location="tests/units/test_state.py:1528">
P3: The new caching tests (test_get_skip_vars_cached_per_class, test_frontend_computed_var_names_cached_per_class) verify the cache populates and is per-class, but never verify invalidation, even though the PR calls out that get_skip_vars must be invalidated on add_var/_update_substate_inherited_vars and frontend computed var names on computed_vars change. A test that calls get_skip_vars, then add_var, and re-checks would guard the invalidation paths that the caching optimization depends on.</violation>
</file>

<file name="reflex/state.py">

<violation number="1" location="reflex/state.py:1301">
P2: When `add_var` adds a field to a state with grandchildren, the grandchildren keep a stale `_skip_var_names` cache because this invalidation covers only direct children. Their aliased `inherited_vars` contains the new field, but `get_skip_vars()` does not, so proxy assignment can take the wrong immutable/non-persisted path. Invalidate skip caches recursively, or reuse `_update_substate_inherited_vars` for this update.</violation>
</file>

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

Re-trigger cubic

Comment thread reflex/state.py
for substate_class in cls.get_substates():
substate_class.vars.setdefault(name, var)
# inherited_vars may alias this class's vars dict.
substate_class._skip_var_names = None

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.

P2: When add_var adds a field to a state with grandchildren, the grandchildren keep a stale _skip_var_names cache because this invalidation covers only direct children. Their aliased inherited_vars contains the new field, but get_skip_vars() does not, so proxy assignment can take the wrong immutable/non-persisted path. Invalidate skip caches recursively, or reuse _update_substate_inherited_vars for this update.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At reflex/state.py, line 1301:

<comment>When `add_var` adds a field to a state with grandchildren, the grandchildren keep a stale `_skip_var_names` cache because this invalidation covers only direct children. Their aliased `inherited_vars` contains the new field, but `get_skip_vars()` does not, so proxy assignment can take the wrong immutable/non-persisted path. Invalidate skip caches recursively, or reuse `_update_substate_inherited_vars` for this update.</comment>

<file context>
@@ -1262,6 +1297,8 @@ def add_var(cls, name: str, type_: Any, default_value: Any = None):
         for substate_class in cls.get_substates():
             substate_class.vars.setdefault(name, var)
+            # inherited_vars may alias this class's vars dict.
+            substate_class._skip_var_names = None
 
         # Reinitialize dependency tracking dicts.
</file context>

Comment thread tests/units/test_state.py
child_skip = SkipVarsChildState.get_skip_vars()
assert child_skip is not parent_skip
assert "p" in child_skip
assert "p" not in parent_skip

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.

P3: The new caching tests (test_get_skip_vars_cached_per_class, test_frontend_computed_var_names_cached_per_class) verify the cache populates and is per-class, but never verify invalidation, even though the PR calls out that get_skip_vars must be invalidated on add_var/_update_substate_inherited_vars and frontend computed var names on computed_vars change. A test that calls get_skip_vars, then add_var, and re-checks would guard the invalidation paths that the caching optimization depends on.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/units/test_state.py, line 1528:

<comment>The new caching tests (test_get_skip_vars_cached_per_class, test_frontend_computed_var_names_cached_per_class) verify the cache populates and is per-class, but never verify invalidation, even though the PR calls out that get_skip_vars must be invalidated on add_var/_update_substate_inherited_vars and frontend computed var names on computed_vars change. A test that calls get_skip_vars, then add_var, and re-checks would guard the invalidation paths that the caching optimization depends on.</comment>

<file context>
@@ -1510,6 +1510,57 @@ def timed(self) -> int:
+    child_skip = SkipVarsChildState.get_skip_vars()
+    assert child_skip is not parent_skip
+    assert "p" in child_skip
+    assert "p" not in parent_skip
+
+
</file context>

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