Skip to content

Skip deep type-validation on state var hot paths - #6738

Closed
Alek99 wants to merge 3 commits into
claude/reflex-compiler-perf-t8ztc9-11-memoize-dedupfrom
claude/reflex-perf-optimizations-01l7a3-eng-10093
Closed

Skip deep type-validation on state var hot paths#6738
Alek99 wants to merge 3 commits into
claude/reflex-compiler-perf-t8ztc9-11-memoize-dedupfrom
claude/reflex-perf-optimizations-01l7a3-eng-10093

Conversation

@Alek99

@Alek99 Alek99 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Linear: ENG-10093

Description

Two hot paths ran _isinstance(value, type, nested=1), which walks every element of list/dict values, only to gate a diagnostic log that never changes behavior:

  • BaseState.__setattr__ validated the full container on every assignment.
  • ComputedVar.__get__ / AsyncComputedVar.__get__ validated the return type on every access, including cache hits.

Changes:

  • Computed var return types are now validated only when the value is actually recomputed (sync and async). Cache hits skip the check entirely.
  • Element-wise validation depth is now 1 in dev and 0 in prod (new cached _validation_depth() helper in reflex_base.utils.types), so production only checks the outer type. Dev keeps the exact same diagnostics as before.

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

Case main this PR speedup
read cached computed var returning 10k dicts (dev) 13.81 / 14.05 ms 0.0015 / 0.0015 ms ~9,200x
read cached computed var (prod) 13.66 / 13.93 ms 0.0013 / 0.0015 ms ~9,300x
assign 100k-int list (prod) 90.89 / 90.96 ms 0.020 / 0.024 ms ~4,000x
assign 100k-int list (dev) 90.74 / 92.67 ms 91.76 / 94.74 ms unchanged (by design)

cProfile (3 assigns + 20 cached reads, dev): main spends 3.06s in 10.5M calls dominated by _isinstance (510k calls); with this PR the cached-read side disappears (_check_deprecated_return_type no longer on the cache-hit path) and in prod the whole workload is 538 function calls / 0.003s.

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/reflex_base/vars/test_base.py: return type checked on recompute, not on cache hits (sync, async, and cache=False).
    • tests/units/reflex_base/utils/test_types.py: _validation_depth() is 0 in prod / 1 in dev.
    • tests/units/test_state.py: wrong-typed assignment still logs an error in dev.
  • Have you successfully ran tests with your changes locally?

Note: behavior-wise, a wrong-typed computed var now logs once per recompute instead of once per access, and prod no longer walks container elements for the log-only check. No exceptions or control flow depend on these checks.

🤖 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 10, 2026 19:53
@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-10093

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reduces type-validation work on hot state-var paths. The main changes are:

  • Add a validation-depth helper for prod versus dev mode.
  • Use shallow validation for production state assignments and computed returns.
  • Validate cached computed vars only when they recompute.
  • Add tests for depth selection, state assignment logging, and computed-var cache behavior.

Confidence Score: 4/5

This is close, but the computed-var cache behavior should be fixed before merging.

  • The validation-depth helper now follows in-process environment changes.
  • Cached computed vars can still skip the dev recheck after a prod-to-dev mode switch.
  • That leaves a wrong nested return value silent until the var recomputes for another reason.

Files Needing Attention: packages/reflex-base/src/reflex_base/vars/base.py

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/utils/types.py Adds mode-based validation depth that re-reads the raw environment value.
packages/reflex-base/src/reflex_base/vars/base.py Moves computed-var return validation to recompute paths, but cache hits can miss dev diagnostics after a mode switch.
reflex/state.py Uses the new validation depth for state assignment diagnostics.
tests/units/reflex_base/utils/test_types.py Adds coverage for validation depth across prod and dev modes.
tests/units/reflex_base/vars/test_base.py Adds computed-var validation tests for recompute and cache-hit behavior.
tests/units/test_state.py Adds coverage that wrong-typed state assignment still logs in dev mode.

Reviews (5): Last reviewed commit: "Honor runtime env mode changes in _valid..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/utils/types.py
@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×2.5

⚡ 4 improved benchmarks
✅ 23 untouched benchmarks
⏩ 8 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_var_access[mutable_list] 71.3 ms 9.1 ms ×7.8
Simulation test_var_access[mutable_dict] 88.6 ms 19.7 ms ×4.5
Simulation test_evaluate_page_with_hooks[_stateful_page] 5.3 ms 4.7 ms +11.85%
Simulation test_var_access[mutable_dataclass_list] 206.6 ms 195.9 ms +5.49%

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-10093 (1f6a4f4) with claude/reflex-compiler-perf-t8ztc9-11-memoize-dedup (dea5c0c)2

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.

  2. No successful run was found on claude/reflex-compiler-perf-t8ztc9-11-memoize-dedup (460c941) during the generation of this report, so e7fe5e7 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/utils/types.py
setattr(instance, self._last_updated_attr, datetime.datetime.now())
value = getattr(instance, self._cache_attr)
self._check_deprecated_return_type(instance, value)
return value

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.

if we return early, how does the computed value get cached?

@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10093 branch from f75dcde to 1b480cb Compare July 18, 2026 01:46
@Alek99
Alek99 changed the base branch from main to claude/reflex-compiler-perf-t8ztc9-11-memoize-dedup July 18, 2026 01:46
Comment thread packages/reflex-base/src/reflex_base/vars/base.py
claude added 3 commits August 12, 2026 16:19
Assigning a state var and reading a computed var both ran
_isinstance(value, type, nested=1), walking every element of
list/dict values only to gate a diagnostic log. The computed var
check also ran on every access, including cache hits.

- Validate computed var return types only when the value is
  recomputed (sync and async), not on cache hits.
- Validate one container level deep only in dev mode; production
  now checks just the outer type (the check never gates behavior,
  it only logs).

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
Address review feedback: instead of caching the first observed mode
forever, re-read the raw REFLEX_ENV_MODE value on every call and cache
the depth per raw value, so in-process mode changes take effect
immediately at negligible hot-path cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
@Alek99
Alek99 force-pushed the claude/reflex-compiler-perf-t8ztc9-11-memoize-dedup branch from dea5c0c to 460c941 Compare August 12, 2026 23:32
@Alek99
Alek99 force-pushed the claude/reflex-perf-optimizations-01l7a3-eng-10093 branch from 1b480cb to 1f6a4f4 Compare August 12, 2026 23:32
return value

return value
return getattr(instance, self._cache_attr)

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.

P1 Cache Hit Skips Recheck When a cached computed var is first evaluated in prod, nested return values are checked with the shallow depth and then stored. If the same process switches to dev mode without changing the var dependencies, this cache-hit path returns the stored value without calling _check_deprecated_return_type. Dev-mode access can keep returning a nested wrong-typed value with no diagnostic until an unrelated recompute happens. The cache needs to account for validation depth changes, or mode changes need to invalidate these cached values.

@Alek99

Alek99 commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Folded into #6743, which now spans the full state hot-path set — this PR, #6740, and the dirty-propagation frontier all edit the same functions (__setattr__, _get_attribute, ComputedVar.__get__, _mark_dirty*), so they're easier to review as one final shape. Commits are preserved there per original PR.

Re: the open question on the early return — the value is cached by the setattr(instance, self._cache_attr, ...) on the line immediately before that return; the early return only skips re-reading and re-validating the just-computed value. Also answered in #6743's description.

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