perf(handler): borrow render state through scopes - #474
Merged
Mohamed Mansour (mohamedmansour) merged 1 commit intoAug 25, 2026
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mohamed Mansour (mohamedmansour)
marked this pull request as ready for review
August 25, 2026 02:26
Mohamed Mansour (mohamedmansour)
merged commit Aug 25, 2026
b56f6ef
into
microsoft:mohamedmansour-perf-handler-render-fragment-index
1 check passed
Mohamed Mansour (mohamedmansour)
deleted the
mohamedmansour-perf-borrowed-scopes
branch
August 25, 2026 02:36
Mohamed Mansour (mohamedmansour)
added a commit
that referenced
this pull request
Aug 25, 2026
## 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) --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.
Dependency
Depends on #472 (B1). This PR is intentionally stacked on
mohamedmansour-perf-handler-render-fragment-index, must merge after #472, and should be reviewed as the B2-only diff against that branch. Do not rebase this branch while #472 remains open.Summary
unsafecodeCombined stack justification
B1 adds a one-time
Protocol::newcost of +24/+128 µs and +3.6–5.1% retained protocol memory in exchange for precomputed render-fragment indexing. B2 leaves that construction and retained-memory cost unchanged while removing 60.5–83.7% of render allocation calls and reducing measured render latency by 25.7–50.6% on the contact-book/contactspath.The combined B1+B2 stack is favorable: B2 amortizes B1's largest measured construction penalty in one render at 100/1,000 contacts and in roughly six renders at 10 contacts. Output remains byte-identical, completed renders retain zero additional bytes, and all 16 construction fixtures retain exactly the same bytes under B1 and B1+B2.
Performance
Sequential custom-allocator harness, B1 versus B1+B2, contact-book
/contacts:Allocation deltas are deterministic: B2 removes exactly 594, 5,724, and 57,024 allocation calls at the 10/100/1,000 scales for both plugin modes. Net retained bytes per completed render remain zero.
Correctness
RENDER/STREAMharness lines are byte-identical between B1 and B1+B2component-assets path=/ plugin=trueRENDERlineValidation
cargo test -p microsoft-webui-handlercargo xtask check(with same-driveTEMP/TMPrequired by the Windows projection adapter)