Optimize runtime type checks and state delta encoding - #7056
Optimize runtime type checks and state delta encoding#7056FarhanAliRaza wants to merge 6 commits into
Conversation
|
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).
075e619 to
a2d92b8
Compare
Merging this PR will improve performance by 80.06%
|
| 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)
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. ↩
Greptile SummaryThis 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.
Confidence Score: 5/5The 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.
|
| 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
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…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.
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.
Type of change
Description
This PR introduces three performance optimizations for the per-event runtime path:
Runtime type validation via pydantic-core (
runtime_isinstance):_isinstancecheck (isinstance leaves, strict containers, unions, literals)_isinstance()for hints without an exact schema equivalent (key-level TypedDict checks, non-dict Mappings), for Var values, and when pydantic-core is not installedMutableProxyvalues, which pydantic-core's container checks would otherwise reject_isinstance()on the hot paths: state var assignments and computed var resultsCompact JSON encoding for state deltas (
json_dumps_compact):json_dumps; a serializer registered forEnum,UUID, or a subclass switches the wire path back to the stdlib encoder so it is honorednull, so the bareNaN/Infinitytokens the frontend revives are preservedorjsonbecomes a dependency ofreflex-baseMutableProxy fast paths:
_new_proxy()calls the C allocator and setattr slots directly, skipping the Python-level__new__/__init__dispatch per elementMeasurements
End to end on the reflex-dev/templates
dashboardapp through its real backend and a Socket.IO client (median round trip, 10k-rowitems.csv):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 itutils/format.py:json_dumps_compact()utils/serializers.py:overrides_native_json_type()and a bounded per-class dataclass field-name cachevars/base.py: computed var return checks useruntime_isinstance()pyproject.toml: orjson dependencyreflex:
istate/proxy.py: MutableProxy construction, class resolution, scalar reads, iterationstate.py: state var assignment and inline computed var checks useruntime_isinstance()app.py: Socket.IO JSON encoder usesjson_dumps_compact()reflex-components-core:
core/_upload.py: streamed upload updates usejson_dumps_compact()Tests:
tests/units/reflex_base/utils/test_types.py: parity ofruntime_isinstance()with_isinstance()across hints and values, Var handling, proxy unwrapping, class-spoofing objects, validator caching and fallbackstests/units/utils/test_format.py:json_dumps_compact()output parity withjson_dumps(), non-finite floats, big integers, custom and overwritten Enum serializerstests/benchmarks/test_event_processing.py:test_process_table_event, one holistic benchmark of the per-event runtime pathNews 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">