improve compile perf - #6804
Conversation
Greptile SummaryThis PR accelerates the deterministic hashing used for compiler auto-memoization by replacing per-node hasher updates with a buffer-based accumulation strategy and a memoized, per-type encoder dispatch table. It also fixes a latent correctness issue where components that inherit from a dataclass (e.g.,
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/components/component.py | Replaces the incremental _update_deterministic_hash approach with buffer-based _encode_deterministic, adds a type-keyed encoder table (_ENCODERS) with per-type memoization, caches dataclasses.fields() results via @functools.cache, and adds an identity-keyed encoding cache (_ENCODED_DATACLASSES) for frozen dataclass instances with all-scalar fields. Also fixes a pre-existing ordering bug where components inheriting from a dataclass were hashed by their (often empty) field list instead of their rendered content. The encoder table overwrite ordering is intentional and correct — _encode_frozen_dataclass downgrades its own entry on the first encounter of a mutable-field type, and the downgrade persists from the second call onwards. Logic is sound with no correctness regressions. |
| tests/units/components/test_component.py | Adds 18 focused tests covering: collision resistance, dict-order independence, subclass normalization, Var data inclusion, cache eviction (oldest-first), size cap, frozen-dataclass identity reuse, mutation tracking for non-cacheable types, MutableProxy-style synthesized classes, component-vs-dataclass branch ordering, and _get_component_hash lifecycle-hook sensitivity. The encoding_caches fixture correctly isolates module-level _ENCODERS and _ENCODED_DATACLASSES state for tests that need it. Test test_deterministic_hash_handles_dataclasses_without_params references _HashFrozenScalars defined later in the file, which is valid in Python (function-body lookup is deferred to call time) but may surprise readers. |
| packages/reflex-base/news/6804.performance.md | Changelog entry accurately describing the 3.7× speedup on large page hashing, the per-type encoder table, identity-keyed frozen-dataclass encoding reuse, and the fix for MarkdownComponentMap-inheriting components. |
Reviews (11): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile
Merging this PR will improve performance by 5.57%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | test_compile_all_artifacts[_stateful_page] |
27 ms | 25.4 ms | +6.35% |
| ⚡ | Simulation | test_compile_page[_stateful_page] |
30.6 ms | 28.9 ms | +5.8% |
| ⚡ | Simulation | test_compile_page_full_context[_stateful_page] |
34.6 ms | 33.1 ms | +4.58% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing benedikt-bartscher:improve-compile-perf (a642daa) with main (f7c848f)2
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. ↩
-
No successful run was found on
main(45b8ed5) during the generation of this report, so f7c848f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/units/components/test_component.py">
<violation number="1" location="tests/units/components/test_component.py:2475">
P3: This test's name and docstring claim it verifies that the frozen-dataclass encoding cache is reused by identity, but the two equality assertions would pass even if no cache existed at all (equal-but-distinct instances always encode to the same bytes). Either rename/reword it as a plain equality contract test, or actually exercise the cache path, e.g. clear component._ENCODED_DATACLASSES, hash `shared` once, then assert id(shared) is present in the cache and that a second hash of the same object short-circuits.</violation>
</file>
<file name="packages/reflex-base/src/reflex_base/components/component.py">
<violation number="1" location="packages/reflex-base/src/reflex_base/components/component.py:688">
P2: The new `_ENCODED_DATACLASSES` cache pins up to 8192 frozen dataclass instances and copies of their encoded bytes in a module-global for the whole process lifetime, released only by a full `clear()` once the cap is hit. Because `_deterministic_hash` runs for every component hash during a compile, these scalar-only frozen instances (and their otherwise-transient byte encodings) no longer get garbage-collected, adding persistent memory that was not retained before. The full-clear-on-capacity eviction also drops the entire cache at once and forces re-encoding of the whole working set right at the boundary, so the cache both holds memory and thrashes. Consider evicting/limiting per-entry (e.g. only cache the bytes keyed by object identity while bounding total retained bytes, or evict entries gradually instead of a wholesale clear).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…t it did (reflex-dev#6948) * ENG-11237 feat(hosting-cli): report why a deploy failed, not just that it did The watch loop decided everything by substring against a bare status string, and a build failure printed two warnings: the raw status, and an unconditional pointer at `reflex cloud apps build-logs`. A generic failure printed the status alone and nothing else. That pointer was unconditional because there was nothing to condition it on. The server classifies every failure as the app's, the platform's, or transient, but that classification never reached a client -- so a failure in the build pipeline arrived dressed as a build failure and sent people looking for a bug in an app that did not have one. The failure arms now fetch GET /deployments/{id}/failure and print the recorded reason, the guidance for that fault, and the end of the build log when the code is one the log explains. The excerpt goes through console.print(markup=False): it is raw build output, and rich would read its paths and version specifiers as markup. Every way of not getting an answer is one case -- a server predating the endpoint 404s, an older self-hosted one may not route it, the network may be down -- and all three fall back to exactly what the arm printed before, so a new CLI against an older server is unchanged. * Strip terminal controls from the excerpt, and file the news fragment per package Two fixes from review. The excerpt is raw build output -- the user's own dependencies and build scripts -- and it is now printed without anyone asking, on any failed deploy, where before it took an explicit `reflex cloud apps build-logs`. markup=False stops rich reading the text as its own markup and does nothing about escape sequences, so OSC 52 could write the reader's clipboard, OSC 8 could render one destination and link to another, and CSI could erase the lines above it and leave "build succeeded" on screen. Colour is not worth carrying for output shown unsolicited. The changelog job runs towncrier per affected package, so a fragment for a change under packages/reflex-hosting-cli/src has to live in that package's own news directory, not the repository root's. * Widen the escape class, and let a malformed answer fall back like any other Two review findings, both narrow and both real. The two-character escape class covered ESC + 0x40-0x5F, so a sequence whose final byte falls outside it -- `\x1b7` (DECSC), `\x1bc` (a full terminal reset) -- had its ESC removed by the bare-control catch-all and printed the final byte as a stray character. Inert, since the ESC is what drives the terminal, but it is garbage in an excerpt whose whole job is to be read. The general ECMA-48 shape covers them. `response.json()` raises UnicodeDecodeError on a 2xx body in an encoding httpx cannot decode, and that is a ValueError rather than a JSONDecodeError, so it escaped the fallback and would have ended the watch over a malformed answer to a request whose contract is that not getting one costs nothing. The excerpt's type is checked for the same reason: the CLI ships apart from the control plane and talks to self-hosted ones. * Report a build log the server could not read, rather than passing over it The failure endpoint now separates an unreadable log from a build that stored none. Collapsing the two tells somebody their build produced no log when the store was simply down, so the two get different answers here. * Assert the no-log path offers no build log, not just no outage message The test claimed the reason stands alone and only checked the outage wording. Offering the command is what separates this path from the unreadable one, so that is what has to be absent. Verified by mutation: forcing the offer fails this test and nothing else. * Fall back however the failure body is malformed RecursionError is a RuntimeError, so a deeply nested document escaped the ValueError catch and aborted the deploy watch -- over an answer this function is contracted to treat as no answer at all. Parametrized with the UnicodeDecodeError case, since they are one rule.
|
seems i haev some overlap here with #6947 |
yeah, maybe we can try to get best of both worlds? |
No description provided.