Skip to content

perf(handler): borrow render state through scopes - #474

Merged
Mohamed Mansour (mohamedmansour) merged 1 commit into
microsoft:mohamedmansour-perf-handler-render-fragment-indexfrom
mohamedmansour:mohamedmansour-perf-borrowed-scopes
Aug 25, 2026
Merged

perf(handler): borrow render state through scopes#474
Mohamed Mansour (mohamedmansour) merged 1 commit into
microsoft:mohamedmansour-perf-handler-render-fragment-indexfrom
mohamedmansour:mohamedmansour-perf-borrowed-scopes

Conversation

@mohamedmansour

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

Copy link
Copy Markdown
Contributor

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

  • borrow state-backed component attributes and loop items through render scopes instead of cloning JSON subtrees
  • preserve lexical resolver precedence across nested loops, component-local state, and global fallback
  • use four inline borrowed-scope slots plus a bounded overflow pool; progressive streaming materializes borrowed attrs only at the host-call lifetime boundary
  • keep finalized B1's numeric render/continuation slots unchanged; no unsafe code

Combined stack justification

B1 adds a one-time Protocol::new cost 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 /contacts path.

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:

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

  • 90 deterministic RENDER/STREAM harness lines are byte-identical between B1 and B1+B2
  • excluded only the documented nondeterministic component-assets path=/ plugin=true RENDER line
  • final source review removed a streaming-only duplicate resolver lookup while retaining owned continuation state

Validation

  • cargo test -p microsoft-webui-handler
  • cargo xtask check (with same-drive TEMP/TMP required by the Windows projection adapter)

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:26
@mohamedmansour
Mohamed Mansour (mohamedmansour) merged commit b56f6ef into microsoft:mohamedmansour-perf-handler-render-fragment-index Aug 25, 2026
1 check passed
@mohamedmansour
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>
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.

1 participant