Skip to content

improve compile perf - #6804

Open
benedikt-bartscher wants to merge 13 commits into
reflex-dev:mainfrom
benedikt-bartscher:improve-compile-perf
Open

improve compile perf#6804
benedikt-bartscher wants to merge 13 commits into
reflex-dev:mainfrom
benedikt-bartscher:improve-compile-perf

Conversation

@benedikt-bartscher

Copy link
Copy Markdown
Contributor

No description provided.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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., rx.text via MarkdownComponentMap) were hashed against their (often empty) field list rather than their rendered content.

  • Encoder table + bytearray buffer: _update_deterministic_hash (one hasher.update per tree node) is replaced by _encode_deterministic which fills a bytearray, flushed into the MD5 hasher once per top-level value. A per-type _ENCODERS dict is populated lazily so subsequent calls avoid isinstance chains entirely.
  • Frozen-dataclass identity cache: _ENCODED_DATACLASSES (an OrderedDict with bounded capacity) caches the encoded bytes of frozen dataclass instances whose fields are all immutable scalars, keyed by object identity. A strong reference in the cache entry prevents id reuse while the entry lives.
  • dataclasses.fields() caching: _dataclass_fields_to_encode wraps the per-call tuple rebuild in @functools.cache, eliminating millions of redundant allocations on large apps.

Confidence Score: 5/5

  • Safe to merge — the hash encoding is functionally equivalent to the old path for all existing types, the correctness fix for dataclass-inheriting components is well-targeted, and the 18 new tests cover the edge cases thoroughly.
  • The buffer-based encoding produces byte-for-byte identical output to the old incremental hasher for every supported type. The encoder table's overwrite ordering is intentional and correct: _encode_frozen_dataclass downgrades its own _ENCODERS entry on the first encounter of a mutable-field type, and the entry is not overwritten a second time because the memoization block only runs when the encoder came from _resolve_encoder, not from the table. The frozen-dataclass identity cache is bounded in both entry count and per-entry size, and strong references prevent stale id lookups. The _get_component_hash refactor preserves the original value ordering and the semantics of the shallow/deep split.
  • No files require special attention.

Important Files Changed

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

Comment thread packages/reflex-base/src/reflex_base/components/component.py Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 5.57%

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

⚡ 3 improved benchmarks
✅ 24 untouched benchmarks
⏩ 8 skipped benchmarks1

Performance Changes

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

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

@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review July 21, 2026 11:09
@benedikt-bartscher
benedikt-bartscher requested a review from a team as a code owner July 21, 2026 11:09

@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 across 2 files

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

Fix all with cubic | Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/components/component.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.

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

Comment thread packages/reflex-base/src/reflex_base/components/component.py Outdated
Comment thread tests/units/components/test_component.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 across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/units/components/test_component.py
benedikt-bartscher and others added 5 commits August 21, 2026 00:38
…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.
@masenf

masenf commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

seems i haev some overlap here with #6947

@benedikt-bartscher

Copy link
Copy Markdown
Contributor Author

seems i haev some overlap here with #6947

yeah, maybe we can try to get best of both worlds?

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