Skip to content

Reduce MutableProxy per-element overhead and cache per-field proxies#6740

Open
Alek99 wants to merge 4 commits into
mainfrom
claude/reflex-perf-optimizations-01l7a3-eng-10094
Open

Reduce MutableProxy per-element overhead and cache per-field proxies#6740
Alek99 wants to merge 4 commits into
mainfrom
claude/reflex-perf-optimizations-01l7a3-eng-10094

Conversation

@Alek99

@Alek99 Alek99 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Linear: ENG-10094

Description

  • MutableProxy._wrap_recursive walked 5 stack frames (_is_called_from_dataclasses_internal) for every element retrieved through a proxy, before the cheap is_mutable_type check that usually decides nothing needs wrapping. The checks are reordered so immutable elements (the common case: ints, strings in a proxied list) skip the frame walk entirely. The dataclasses-internal check still runs for mutable values, preserving dataclasses.asdict/astuple behavior.
  • BaseState._get_attribute constructed a fresh MutableProxy on every read of a mutable state var. The proxy is now cached per instance and field name, invalidated by identity when the underlying value is reassigned. The cache is excluded from pickling (__getstate__), and copy/deepcopy already round-trip through __getstate__, so copies never share or carry proxies.

One deliberate nuance: when _wrap_recursive receives an already-proxied value and is called from dataclasses internals, it now returns the unwrapped value (previously the still-wrapped proxy), matching the documented intent of that path.

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

Case main this PR speedup
iterate 1000-int list via proxy 0.977 / 0.973 ms 0.219 / 0.214 ms ~4.5x
index 1000 elements via proxy 1.204 / 1.171 ms 0.396 / 0.381 ms ~3.0x
read mutable var attribute 2.82 / 2.87 us 1.17 / 1.15 us ~2.4x

cProfile (20 iterations + 2000 attr reads): _is_called_from_dataclasses_internal (20,000 calls, 0.028s cumulative) disappears from the profile; total function calls drop from 168k to 72k.

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?
    • tests/units/istate/test_proxy.py: proxy reused across reads and invalidated on reassignment; cache never serialized (pickle round-trip rebinds proxies to the restored state); iteration yields plain immutables while mutable elements stay wrapped.
  • Have you successfully ran tests with your changes locally?

🤖 Generated with Claude Code

https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X


Generated by Claude Code

_wrap_recursive walked 5 stack frames (dataclasses-internal check) for
every element retrieved through a proxy, even immutable scalars that
never get wrapped. Check is_mutable_type first so immutable elements
skip the frame walk entirely.

Also cache the MutableProxy built for each mutable state var on the
instance, keyed by field name, instead of constructing a fresh proxy
on every attribute read. The cache is invalidated by identity when the
underlying value is reassigned and is excluded from pickling (copy and
deepcopy already go through __getstate__).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
@Alek99 Alek99 requested a review from a team as a code owner July 10, 2026 20:03
@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-10094

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces overhead in mutable state proxy handling. The main changes are:

  • Skips dataclasses frame checks for immutable proxied values.
  • Caches mutable state proxies per instance and field.
  • Clears cached proxies when state fields are reassigned.
  • Excludes proxy caches from serialized state.
  • Reserves the internal proxy cache field name.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
reflex/istate/proxy.py Reorders recursive proxy wrapping so immutable values return before the dataclasses frame check.
reflex/state.py Adds a per-field mutable proxy cache with reassignment invalidation and serialization cleanup.
packages/reflex-base/src/reflex_base/utils/types.py Adds _mutable_proxy_cache to the reserved backend variable names.
tests/units/istate/test_proxy.py Adds tests for proxy cache reuse, cache serialization, and immutable iteration.
tests/units/test_state.py Updates the state proxy identity expectation for cached mutable proxies.

Reviews (4): Last reviewed commit: "Declare _mutable_proxy_cache as a reserv..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×3.2

⚡ 2 improved benchmarks
✅ 24 untouched benchmarks
⏩ 8 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_var_access[mutable_list] 71.6 ms 19.1 ms ×3.8
test_var_access[mutable_dict] 88.6 ms 32.5 ms ×2.7

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/reflex-perf-optimizations-01l7a3-eng-10094 (d6ab47c) with main (48f9e6c)

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.

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

ℹ️ 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 Outdated
Address review feedback: drop the _mutable_proxy_cache entry when a
base or backend var is assigned, so the replaced value is not kept
alive by the cached proxy until the next read. Update
test_set_base_field_via_setter for the new cached-proxy identity
semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Comment thread reflex/state.py Outdated
):
# track changes in mutable containers (list, dict, set, etc)
return MutableProxy(wrapped=value, state=self, field_name=name)
cache = super().__getattribute__("__dict__").get("_mutable_proxy_cache")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

similar to the last PR, unclear on why these cache flags are being directly added into the __dict__ and using object.__setattr__ and the like instead of just defining them as reserved fields on the state.

if we define them as real fields on the state, then users will get an exception if they try to reuse the name, instead of internal tracking machinery just getting broken or corrupting user data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good call — converted in d6ab47c: _mutable_proxy_cache is now a declared field(default_factory=dict, is_var=False) like _backend_vars/_was_touched, added to RESERVED_BACKEND_VAR_NAMES so a user var with that name can't silently collide. It stays out of pickles (__getstate__ pop) and is recreated empty in __setstate__. I'll apply the same treatment to the propagation-frontier fields in #6743.


Generated by Claude Code

Address review feedback: instead of managing the proxy cache as an
ad-hoc instance __dict__ entry, declare it as a real (non-var) field
like _backend_vars/_was_touched and add it to
RESERVED_BACKEND_VAR_NAMES, so user vars cannot silently collide with
the framework's tracking machinery. The cache is still excluded from
pickling and recreated empty on unpickle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtmC37Y8kJ5S6
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.

3 participants