Skip to content

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

Open
Alek99 wants to merge 3 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 3 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 MutableProxy overhead for common state reads. The main changes are:

  • Immutable proxy elements skip the dataclasses stack-frame check.
  • Mutable state fields reuse a cached proxy per state instance and field.
  • Cached proxies are invalidated on reassignment and removed from serialized state.
  • Unit tests cover cache reuse, reassignment, serialization, and iteration behavior.

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 wrapping so immutable values return before the dataclasses frame walk while mutable values still follow the wrapping path.
reflex/state.py Adds per-field mutable proxy caching with identity checks, reassignment invalidation, and serialization cleanup.
tests/units/istate/test_proxy.py Adds tests for cached proxy reuse, cache eviction, pickle behavior, and immutable iteration values.
tests/units/test_state.py Updates the mutable proxy identity expectation to match per-field caching.
news/6740.performance.md Adds a changelog note for the MutableProxy performance improvement.

Reviews (3): Last reviewed commit: "Evict cached proxy on reassignment; upda..." | 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

⚠️ 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

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

Performance Changes

Benchmark BASE HEAD Efficiency
test_var_access[mutable_list] 70.9 ms 19.1 ms ×3.7
test_var_access[mutable_dict] 88.2 ms 32.3 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 (e07dff3) with main (9a5c4d3)

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
Comment on lines +1488 to +1490
if not isinstance(proxy, MutableProxy) or proxy.__wrapped__ is not value:
proxy = MutableProxy(wrapped=value, state=self, field_name=name)
cache[name] = proxy

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 Clear cached proxies when mutable vars are reassigned

Because this cache is only checked from the mutable-value read path, a previously read field keeps its cached proxy after __setattr__ replaces the field. In handlers that read a large mutable var and then set it to a new object (or to None/another immutable value) without reading it again, _mutable_proxy_cache[name] still strongly references the old object through proxy.__wrapped__ for the lifetime of the state instance. Please drop or update the cache entry in the assignment path for base/backend vars.

Useful? React with 👍 / 👎.

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