Reduce MutableProxy per-element overhead and cache per-field proxies#6740
Reduce MutableProxy per-element overhead and cache per-field proxies#6740Alek99 wants to merge 3 commits into
Conversation
_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
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Greptile SummaryThis PR reduces MutableProxy overhead for common state reads. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "Evict cached proxy on reassignment; upda..." | Re-trigger Greptile |
Merging this PR will improve performance by ×3.2
|
| 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)
Footnotes
-
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. ↩
There was a problem hiding this comment.
💡 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".
| if not isinstance(proxy, MutableProxy) or proxy.__wrapped__ is not value: | ||
| proxy = MutableProxy(wrapped=value, state=self, field_name=name) | ||
| cache[name] = proxy |
There was a problem hiding this comment.
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
Linear: ENG-10094
Description
MutableProxy._wrap_recursivewalked 5 stack frames (_is_called_from_dataclasses_internal) for every element retrieved through a proxy, before the cheapis_mutable_typecheck 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, preservingdataclasses.asdict/astuplebehavior.BaseState._get_attributeconstructed a freshMutableProxyon 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__), andcopy/deepcopyalready round-trip through__getstate__, so copies never share or carry proxies.One deliberate nuance: when
_wrap_recursivereceives 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)
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
Changes To Core Features:
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.🤖 Generated with Claude Code
https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
Generated by Claude Code