Skip to content

perf(handler): reduce render lookup and scope allocations - #472

Merged
Mohamed Mansour (mohamedmansour) merged 2 commits into
mainfrom
mohamedmansour-perf-handler-render-fragment-index
Aug 25, 2026
Merged

perf(handler): reduce render lookup and scope allocations#472
Mohamed Mansour (mohamedmansour) merged 2 commits into
mainfrom
mohamedmansour-perf-handler-render-fragment-index

Conversation

@mohamedmansour

@mohamedmansour Mohamed Mansour (mohamedmansour) commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Verdict

GO as one combined performance change. This PR now contains both the render-fragment index and borrowed render scopes. It consolidates and supersedes #474.

Summary

  • precompute a compact render-fragment index once in Protocol::new, replacing repeated fragment-target hashes, component prop-name conversion, and unnecessary sibling-route scans with numeric slots and flat metadata arenas
  • borrow state-backed loop items and component attributes through ordinary render scopes instead of cloning JSON subtrees
  • preserve lexical precedence across nested loops, component locals, and global fallback
  • materialize borrowed component attributes only where progressive streaming must retain them across host calls
  • keep all new APIs crate-private; no protobuf, public API, or unsafe changes

Combined performance case

B1 alone moved work from render time to protocol construction. It improved representative fragment-heavy renders by roughly 2–5%, but added a one-time Protocol::new cost of +24 µs for 128 fragments and +128 µs for 512 fragments, plus +3.6–5.1% retained protocol memory. It also had one measured all-miss condition case at +4.15%.

B2 makes the combined stack clearly favorable. Sequential custom-allocator measurements below compare B1 with this combined B1+B2 branch on the contact-book /contacts route:

Contacts FAST Allocations Allocated bytes Median render Throughput
10 no 814 → 220 (-73.0%) 36,143 → 9,829 (-72.8%) 58.855 → 36.432 µs (-38.1%) +61.5%
10 yes 982 → 388 (-60.5%) 86,420 → 60,106 (-30.4%) 84.353 → 62.702 µs (-25.7%) +34.5%
100 no 6,934 → 1,210 (-82.5%) 277,972 → 33,049 (-88.1%) 466.528 → 258.856 µs (-44.5%) +80.2%
100 yes 7,104 → 1,380 (-80.6%) 426,553 → 181,630 (-57.4%) 560.254 → 355.026 µs (-36.6%) +57.8%
1,000 no 68,134 → 11,110 (-83.7%) 2,713,293 → 267,949 (-90.1%) 5.038 → 2.491 ms (-50.6%) +102.3%
1,000 yes 68,308 → 11,284 (-83.5%) 4,827,954 → 2,382,610 (-50.6%) 6.434 → 3.789 ms (-41.1%) +69.8%

Allocation deltas are deterministic: exactly 594, 5,724, and 57,024 allocation calls are removed at the 10/100/1,000 scales in both plugin modes. Timing is corroboration; the allocation counts are the primary evidence.

B2 does not hide B1's construction tradeoff. All 16 construction fixtures retain exactly the same bytes under B1 and combined B1+B2 (contact-book remains 102,619 bytes; the 200-route synthetic remains 574,831 bytes). The combined render savings amortize B1's largest measured construction penalty in one render at 100/1,000 contacts and in roughly six renders at 10 contacts. Completed renders retain zero additional bytes.

For the isolated B1 layer, minimum-time measurements against the original base recorded loop_scaling/500 -5.54%, plugin_fast/without_plugin -4.89%, and loop_scaling/100 -4.58%. The combined measurements above then show the larger B2 gain on top of B1.

Correctness

  • 90 deterministic RENDER/STREAM harness lines are byte-identical between B1 and combined B1+B2
  • excluded only the documented nondeterministic component-assets path=/ plugin=true line, which flips between the same two hashes on the original base
  • retained protocol bytes are unchanged between B1 and combined B1+B2 for all 16 fixtures
  • resolver tests cover borrowed global collections, borrowed component collections, nested loops, owned-local precedence, lexical shadowing, and component isolation
  • progressive streaming keeps owned continuation state and avoids a duplicate resolver lookup

Implementation notes

RenderFragmentIndex reuses interned fragment IDs, stores metadata and canonical component prop names in flat arenas, tracks route presence in a bitset, and resolves fragment lists lazily through eight inline memo slots plus an on-demand spill. Render and continuation slots use the same sorted fragment-ID numbering.

Ordinary rendering uses BorrowedScope, LoopBinding, and VisibleLoopScope to retain references into immutable request state. Four inline borrowed-scope slots cover the common component-attribute case; overflow storage and request-local pools remain bounded. Owned fallback paths preserve synthetic values and continuation lifetimes.

Validation

  • cargo test -p microsoft-webui-handler
  • cargo xtask check — all phases passed, including clippy, deny, workspace tests, native/WASM builds, examples, benchmark validation, and docs (using same-drive TEMP/TMP required by the Windows projection adapter)

Prepare a per-protocol render fragment index once at load so the render
path resolves fragment descents, component prop names and route presence
through numeric slots instead of repeating string hash lookups and
sibling-route scans on every fragment.

Protocol::new builds a flat index that shares the already interned
fragment id Arcs and packs metadata, component prop names and route
presence bits into single arenas, so large protocols are indexed without
duplicating fragment graphs. Render resolves the index once per request
into a small inline memo cache that spills lazily for protocols with more
fragment lists than the cache holds.

Output is byte-identical for buffered and streaming rendering, missing
fragment and component references keep returning the same typed errors,
and no public API or protobuf wire format changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mohamedmansour
Mohamed Mansour (mohamedmansour) marked this pull request as ready for review August 25, 2026 02:27
@mohamedmansour Mohamed Mansour (mohamedmansour) changed the title refactor(handler): precompute a render fragment index perf(handler): reduce render lookup and scope allocations Aug 25, 2026
@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit 1cdc3b3 into main Aug 25, 2026
35 checks passed
@mohamedmansour
Mohamed Mansour (mohamedmansour) deleted the mohamedmansour-perf-handler-render-fragment-index branch August 25, 2026 02:57
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.

2 participants