Skip to content

Optimize runtime type checks and state delta encoding - #7056

Open
FarhanAliRaza wants to merge 6 commits into
mainfrom
claude/performance-optimization-rust-u1afuo
Open

Optimize runtime type checks and state delta encoding#7056
FarhanAliRaza wants to merge 6 commits into
mainfrom
claude/performance-optimization-rust-u1afuo

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Type of change

  • New feature (non-breaking change which adds functionality)
  • Performance improvement

Description

This PR introduces three performance optimizations for the per-event runtime path:

  1. Runtime type validation via pydantic-core (runtime_isinstance):

    • Compiles each state var / computed var type hint once into a pydantic-core validator with the same semantics as the one-level _isinstance check (isinstance leaves, strict containers, unions, literals)
    • Caches compiled validators per type hint
    • Falls back to _isinstance() for hints without an exact schema equivalent (key-level TypedDict checks, non-dict Mappings), for Var values, and when pydantic-core is not installed
    • Unwraps MutableProxy values, which pydantic-core's container checks would otherwise reject
    • Replaces _isinstance() on the hot paths: state var assignments and computed var results
  2. Compact JSON encoding for state deltas (json_dumps_compact):

    • Encodes deltas sent over Socket.IO and streamed upload updates with orjson
    • Dataclasses and datetimes keep going through the reflex serializers so output matches json_dumps; a serializer registered for Enum, UUID, or a subclass switches the wire path back to the stdlib encoder so it is honored
    • Falls back to the stdlib encoder for integers beyond 64 bits and whenever the payload contains a null, so the bare NaN/Infinity tokens the frontend revives are preserved
    • orjson becomes a dependency of reflex-base
  3. MutableProxy fast paths:

    • Resolves the proxy class per (base proxy, wrapped type) through one cached dict lookup
    • _new_proxy() calls the C allocator and setattr slots directly, skipping the Python-level __new__/__init__ dispatch per element
    • Runs the dataclasses-internal check once per iteration instead of per element
    • Returns str/int/float/bool/None values from attribute, item, and iteration reads before touching any wrapping machinery

Measurements

End to end on the reflex-dev/templates dashboard app through its real backend and a Socket.IO client (median round trip, 10k-row items.csv):

Event main this branch
sort_by_payment 85.8 ms 47.9 ms
toggle_sort 134 ms 83.0 ms
next_page 9.35 ms 3.45 ms
all events, CPU per event 84.5 ms 56.1 ms

New holistic benchmark test_process_table_event (three filter events on a 1000-row table through the real event processor, deltas encoded for the wire): main 26.6 ms, this branch 13.7 ms.

Changes

reflex-base:

  • utils/types.py: runtime_isinstance() and the schema compilation behind it
  • utils/format.py: json_dumps_compact()
  • utils/serializers.py: overrides_native_json_type() and a bounded per-class dataclass field-name cache
  • vars/base.py: computed var return checks use runtime_isinstance()
  • pyproject.toml: orjson dependency

reflex:

  • istate/proxy.py: MutableProxy construction, class resolution, scalar reads, iteration
  • state.py: state var assignment and inline computed var checks use runtime_isinstance()
  • app.py: Socket.IO JSON encoder uses json_dumps_compact()

reflex-components-core:

  • core/_upload.py: streamed upload updates use json_dumps_compact()

Tests:

  • tests/units/reflex_base/utils/test_types.py: parity of runtime_isinstance() with _isinstance() across hints and values, Var handling, proxy unwrapping, class-spoofing objects, validator caching and fallbacks
  • tests/units/utils/test_format.py: json_dumps_compact() output parity with json_dumps(), non-finite floats, big integers, custom and overwritten Enum serializers
  • tests/benchmarks/test_event_processing.py: test_process_table_event, one holistic benchmark of the per-event runtime path

News fragments: news/, packages/reflex-base/news/, packages/reflex-components-core/news/

<source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">

@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner September 7, 2026 16:56
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…d orjson

State var assignments and computed var reads validate their value against the
declared type one level deep. That check walked every element in pure Python
(`_isinstance`) on every event, and dominated event processing for large
lists. `runtime_isinstance` compiles each type hint once into a pydantic-core
validator with identical semantics (isinstance leaves, strict containers,
unions, literals) and falls back to `_isinstance` for hints without an exact
schema equivalent, Var values, and when pydantic-core is not installed.
MutableProxy values are unwrapped first since pydantic-core's container checks
do not honor the proxied `__class__`.

State deltas sent over Socket.IO and streamed upload updates are now encoded
by orjson through `format.json_dumps_compact`. Dataclasses and datetimes keep
going through the reflex serializers so the output matches `json_dumps`, and
a serializer registered for an Enum or UUID subclass switches the wire path
back to the stdlib encoder so it is still honored. Integers beyond 64 bits
also fall back. Non-finite floats now serialize as null instead of the
invalid NaN/Infinity tokens. The dataclass serializer memoizes field names
per class.

Measured on the reflex-dev/templates dashboard app end to end through a
Socket.IO client (median round trip, 10k-row items.csv): next_page 9.4 ms
-> 2.7 ms, toggle_sort 134 ms -> 97 ms, overall 84 ms -> 68 ms per event.
Synthetic 10k-item checks: list[int] validation 6.7 ms -> 0.15 ms;
dataclass delta encoding 18 ms -> 5 ms; dict delta encoding 2.7 ms -> 0.3 ms.
Iterating or indexing a list, dict, or dataclass held in a state var wraps
every mutable element in a MutableProxy. Each wrap paid for a Python-level
__new__ (with a dataclass check) and __init__, a five-frame stack walk to
detect dataclasses internals, and a generic mutability lookup even for
scalars.

The proxy class per (base proxy, wrapped type) is now resolved through one
cached dict lookup, and the internal `_new_proxy` constructor calls the C
allocator and setattr slots directly. `__iter__` runs the dataclasses-internal
check once per iteration instead of per element, and `__getattr__`,
`__getitem__` and iteration return str/int/float/bool/None values before
touching any of the wrapping machinery.

Iterating a 10k-element proxied list of dataclasses or dicts drops from about
4.0 us to 1.0 us per element. On the reflex-dev/templates dashboard app with a
10k-row items.csv, the end-to-end median round trip for the sort events fell
from 66 ms to 48 ms and the overall per-event cost from 68 ms to 56 ms
(84 ms on main before this branch).
@FarhanAliRaza
FarhanAliRaza force-pushed the claude/performance-optimization-rust-u1afuo branch from 075e619 to a2d92b8 Compare September 7, 2026 16:58
@codspeed-hq

codspeed-hq Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 80.06%

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

⚡ 4 improved benchmarks
✅ 28 untouched benchmarks
🆕 1 new benchmark
⏩ 8 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_var_access[mutable_dataclass_list] 197.9 ms 64.3 ms ×3.1
test_var_access[mutable_list] 9.1 ms 4.9 ms +86.62%
test_var_access[mutable_dict] 20.1 ms 14 ms +43.24%
test_process_event 5.1 ms 4 ms +27.8%
🆕 test_process_table_event N/A 88.3 ms N/A

Tip

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


Comparing claude/performance-optimization-rust-u1afuo (6907824) with main (c49a85d)

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 Sep 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimizes state processing by compiling shallow runtime type checks with pydantic-core, encoding state updates through an orjson-backed compact serializer, and reducing mutable-proxy construction and iteration overhead.

  • Preserves custom serializer behavior and falls back to stdlib JSON for incompatible values.
  • Applies compact encoding to websocket and buffered-upload state updates.
  • Adds focused compatibility tests and a holistic state-event benchmark.
  • Bounds the newly introduced dataclass metadata cache and safely handles proxy class spoofing.

Confidence Score: 5/5

The PR appears safe to merge; the prior findings are resolved and no new actionable failure remains.

Current code safely tolerates class-spoofing objects, honors overwritten Enum and UUID serializers through the fallback path, and bounds the dataclass metadata cache. The changes since the previous review only restructure and extend benchmarks, and their event completion and workload behavior remain valid.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/utils/types.py Adds cached pydantic-core validators with compatibility fallbacks for unsupported hints, Vars, and wrapped proxy values.
packages/reflex-base/src/reflex_base/utils/format.py Adds compact orjson encoding while preserving Reflex serializer and non-finite-float behavior through targeted fallback.
packages/reflex-base/src/reflex_base/utils/serializers.py Detects serializers that override orjson-native types and introduces a bounded cache for dataclass field names.
reflex/istate/proxy.py Optimizes proxy allocation, class resolution, scalar reads, and iteration without changing mutation tracking semantics.
reflex/app.py Configures Socket.IO to emit compactly encoded state updates while accepting transport-supplied dump options.
packages/reflex-components-core/src/reflex_components_core/core/_upload.py Uses the compact encoder for newline-delimited buffered-upload state updates.
tests/benchmarks/test_event_processing.py Adds a holistic benchmark covering state mutation, computed vars, proxy iteration, delta creation, and wire encoding.

Reviews (5): Last reviewed commit: "Replace micro-benchmarks with one holist..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/utils/types.py Outdated
Comment thread packages/reflex-base/src/reflex_base/utils/serializers.py

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

All reported issues were addressed

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

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/utils/format.py
Comment thread packages/reflex-base/src/reflex_base/utils/types.py
Comment thread packages/reflex-base/src/reflex_base/utils/serializers.py Outdated
Comment thread packages/reflex-base/src/reflex_base/utils/serializers.py Outdated
Comment thread tests/benchmarks/test_json_dumps.py Outdated
…lizers

orjson collapses non-finite floats to null, but the frontend revives the bare
NaN/Infinity tokens json emits (test_computed_vars covers this). The compact
encoder now returns orjson's output only when it contains no null and
otherwise re-encodes with json, so a None or a non-finite float in a delta
takes the exact-output path.

A serializer registered with overwrite=True for Enum or UUID themselves now
also switches the wire path away from orjson; the built-in registrations no
longer count. runtime_isinstance only unwraps objects that carry __wrapped__,
so a test double reporting a foreign __class__ is validated as is instead of
raising.
…enchmark

The per-class dataclass field-name cache no longer evicts at 128 classes,
and the stdlib reference benchmark covers the dict payload as well as the
dataclass one so both compact-encoder cases have a baseline.
Comment thread packages/reflex-base/src/reflex_base/utils/serializers.py Outdated
Unbounded retention would keep dynamically created dataclass types alive
for the process lifetime; 1024 entries never evicts in practice and matches
the bound used for the proxy module's per-type cache.
test_process_table_event runs three filter events on a state holding 1000
dataclass rows through the real event processor and encodes each delta for
the wire the way the socket path does. One event covers the whole per-event
runtime path this branch touches: base var assignment checks, iterating and
sorting proxied rows, computed var recomputation with return-type checks, and
delta encoding. Base branch: 26.6 ms for the three events; this branch: 13.7 ms.

The isolated runtime_isinstance and json_dumps_compact benchmarks are removed
in its favour.
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.

1 participant