perf: dump-then-scan JSON serialization for socket payloads (companion to #6116) - #6826
Closed
FarhanAliRaza wants to merge 33 commits into
Closed
perf: dump-then-scan JSON serialization for socket payloads (companion to #6116)#6826FarhanAliRaza wants to merge 33 commits into
FarhanAliRaza wants to merge 33 commits into
Conversation
β¦llback Happy path uses native JSON.parse; the catch rewrites Python"s bare Infinity/-Infinity/NaN tokens outside string literals before retrying, so only payloads that actually contain specials pay the extra cost. NaN has no JSON literal and becomes null (matches JSON.stringify).
β¦o null Swap bare NaN for a sentinel string before JSON.parse and revive it back to a real NaN so Python-side float(nan) round-trips to the frontend.
Apply orjson_dumps/orjson_loads helpers to the new reflex_base package locations after upstream's restructuring.
test_process_event_wire measures the full receive -> process -> serialize round trip: raw event JSON decoded via orjson_loads (as socket.io's decoder does), events processed through BaseStateEventProcessor with a real StateManagerMemory, and each delta serialized through orjson_dumps_socket the way App._setup_state wires socket.io's encoder. Parametrized with a small (5-row) and large (500-row) table delta so JSON parse/dump cost is part of the measured path, which no existing benchmark covered.
Serialization previously walked every payload with _replace_non_finite_floats before every socket emit to protect NaN/Infinity floats (orjson silently emits null for them) and to escape sentinel-colliding strings. The walk dominated serialization cost (~95% on a 290KB payload) and ran even in the stdlib fallback, making default installs slower than before reflex-dev#6116. Both backends now dump once and decide from the output bytes whether the walk is needed at all: - orjson: None, NaN and Infinity all serialize to exactly the token null, so output without null provably lost no non-finite floats; output without the sentinel prefix has no colliding strings. Only when either substring appears is the payload walked and re-dumped. The default= callback is split into a non-walking pass-1 variant and a walking pass-2 variant so StateUpdate-wrapped payloads keep the fast path. - stdlib (no orjson): bare NaN/Infinity tokens are restored by the frontend's bare-token rewriter, so only a sentinel-prefix hit forces the walk. Restores default-install performance to parity with pre-reflex-dev#6116. Frontend mirror of the same idea: passing a reviver to JSON.parse disables the engine's native fast parser (measured 4-12x slower in Chromium), so parseNonFiniteAwareJSON only applies the reviver when the payload actually contains the sentinel prefix. The upload NDJSON parser gains the bare-token rewriter retry it was missing. Also: hoist the orjson import to module level (was per-call on the compile hot path), make orjson_dumps fall back to stdlib for indent widths other than 2 instead of silently coercing, and preserve kwargs in its TypeError fallback. Serialize-only medians vs stdlib baseline (1000x10 table delta): 1.91ms -> 431us with orjson, parity without. Browser decode of the same delta: 2.05ms -> 506us.
Merging this PR will not alter performance
Performance Changes
Comparing Footnotes
|
Contributor
Greptile SummaryThis PR introduces optional orjson-backed JSON handling and optimizes websocket payload serialization.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains within the scope of the displayed follow-up threads.
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/utils/format.py | Adds optional orjson loading/dumping and socket-specific serialization that preserves non-finite floats through frontend-compatible sentinels. |
| packages/reflex-base/src/reflex_base/.templates/web/utils/state.js | Adds conditional sentinel revival and fallback rewriting for non-standard bare non-finite tokens. |
| packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/upload.js | Replaces JSON5 upload-stream parsing with the shared non-finite-aware JSON parser and fallback rewriter. |
| reflex/app.py | Configures Socket.IO and event processing to use the new optional orjson-backed helpers. |
| packages/reflex-components-core/src/reflex_components_core/core/_upload.py | Uses the new helpers for upload event arguments and streamed state-update serialization. |
| tests/units/utils/test_format_orjson.py | Adds focused coverage for backend selection, custom serialization, non-finite values, sentinel collisions, and fallback behavior. |
| tests/benchmarks/test_event_processing.py | Extends benchmarks to cover complete event decoding, processing, and wire serialization for small and large deltas. |
| pyproject.toml | Adds orjson as an optional project extra and development dependency. |
Reviews (2): Last reviewed commit: "perf: dump-then-scan JSON serialization ..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft companion to #6116 (
try-orjson), branched from its head (7f89aed).Commit 1 adds
test_process_event_wire: a benchmark of the full receive -> process -> serialize round trip (event JSON decoded viaorjson_loads, processed throughBaseStateEventProcessor, delta serialized viaorjson_dumps_socketas socket.io's encoder invokes it), parametrized with 5-row and 500-row table deltas. No existing benchmark exercised the serialization path #6116 changes.Commit 2 (pushed after commit 1's CodSpeed run completes, so the delta is measurable run-over-run) reworks the serialization strategy to dump-then-scan on both backends plus assorted fixes; details in the commit message.
Local walltime medians for the large-delta wire benchmark: 5.01ms before, 3.84ms after (-23%). This PR exists to confirm that delta under CodSpeed instrumentation; findings will be folded back into #6116.