perf: avoid cloning nested route trees - #513
Merged
Mohamed Mansour (mohamedmansour) merged 4 commits intoSep 3, 2026
Merged
Conversation
Copilot started reviewing on behalf of
Mohamed Mansour (mohamedmansour)
September 3, 2026 05:23
View session
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The ownership transitions are coherent, preserve behavior, and have focused regression coverage.
Pull request overview
Avoids deep-cloning nested route subtrees during handler rendering while preserving streaming ownership requirements and existing behavior.
Changes:
- Represents active route levels with
Cow. - Borrows protocol routes and moves already-owned descendants.
- Adds ownership, suspension, and nested-route regression tests.
File summaries
| File | Description |
|---|---|
crates/webui-handler/src/lib.rs |
Implements borrowed and move-based route descent. |
crates/webui-handler/src/streaming/vm.rs |
Preserves owned routes in continuation frames. |
crates/webui-handler/src/streaming/session.rs |
Parks route levels as owned session state and tests retention. |
crates/webui-handler/src/streaming/inventory.rs |
Updates test context initialization for Cow. |
crates/webui-handler/tests/streaming.rs |
Tests nested owned-route descent through streaming rendering. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Mohamed Mansour (mohamedmansour)
requested review from
Bang Lee (Qusic),
Akrosh Gandhi (akroshg),
Jane Chu (janechu) and
mcritzjam
September 3, 2026 05:57
Base automatically changed from
mohamedmansour-reduce-handler-render-allocations
to
main
September 3, 2026 05:58
The render context materialized data it only ever reads from the protocol. `route_children` held `Vec<WebUiFragmentRoute>`, so descending into a matched route deep-cloned the entire remaining route subtree - two strings, the recursive children vector, and the cache-tag and invalidation vectors, for every route on the matched chain of every request. The scope maps, the rendered-component set, and the document-style set were all `String`-keyed even though every key they receive is a component prop name, a `<for>` moniker, or a style resource name that the protocol already owns and that outlives the request. Those fields now hold `Cow<'protocol, _>`, so a render binds names and descends route levels without copying a byte. The owned variant is not removed because a suspended streaming continuation outlives the borrow it rendered against: `SessionCore` detaches what it parks between host calls, which moves every entry and allocates only for names that render newly bound - the same allocation the `String`-keyed map used to pay on insert, just deferred to the suspension that actually needs it. Keeping the owned variant is also what keeps the change safe. The workspace denies `unsafe_code`, and `StreamingSession` owns its `Arc<Protocol>`, so a context lifetime threaded into the session state would have no caller to name it and would need a self-referential struct. `Cow` lets the render path borrow while the session parks something that stands on its own, so the session, the FFI, and the WASM and Python bindings are untouched. An outlet still consumes its route level rather than restoring it, which preserves the previous behavior exactly. | Path | Before | After | Change | |---|---:|---:|---:| | string allocs/run | 141 | 122 | -13.5% | | streaming allocs/run | 155 | 136 | -12.3% | | streaming POOLED allocs/run | 148 | 129 | -12.8% | | string bytes/run | 30.5 KiB | 28.7 KiB | -5.9% | | streaming POOLED bytes/run | 8.7 KiB | 6.9 KiB | -20.7% | | string wall us/run @1000 | 13.60 | 13.22 | -2.8% | | Contact book Render/1000 P50 | 1.52 ms | 1.48 ms | -2.6% | | Output size | 24152 B | 24152 B | unchanged | The contact-book fixture has a shallow route graph, so it understates the route-tree win. On the three-level nested `examples/app/routes` tree, allocations per render drop 58 -> 48 at `/` and 141 -> 128 at `/sections/:id/topics/:id/lessons/:id`, with byte-identical output at every depth. Validation: `cargo xtask check`, `cargo xtask bench streaming-resource`, `cargo bench -p microsoft-webui --bench contact_book_bench`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2edb7e98-5706-46bc-b191-51620cd290e5
The comments read as if consuming the route level were intended design. It is the previous behavior preserved on purpose, and #515 records why it is a bug, so say that and link it rather than leaving a future reader to conclude the emptying is load-bearing. Comment-only; no behavior change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2edb7e98-5706-46bc-b191-51620cd290e5
Keep route children borrowed during ordinary rendering while restoring owned name maps and sets across the handler. Preserve the scope-map pool between streaming suspension steps and materialize route levels before parking them. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2edb7e98-5706-46bc-b191-51620cd290e5
Transfer a parked route's child level into ordinary outlet rendering instead of recursively cloning it. Keep protocol-backed levels borrowed and cover the ContinuationVm-to-ordinary nested outlet path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 07ffb414-b15a-4409-a020-b2e406287de0
Mohamed Mansour (mohamedmansour)
force-pushed
the
mohamedmansour-borrow-protocol-data-in-context
branch
from
September 3, 2026 05:58
99ca3f3 to
bc5f414
Compare
Bang Lee (Qusic)
approved these changes
Sep 3, 2026
Mohamed Mansour (mohamedmansour)
deleted the
mohamedmansour-borrow-protocol-data-in-context
branch
September 3, 2026 23:10
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.
Matched route descent deep-cloned the entire remaining
WebUiFragmentRoutesubtree. Because each route recursively owns strings, child vectors, cache tags, and invalidation data, the cost grows with route depth and sibling count on every request.This change keeps protocol-owned route levels borrowed during ordinary rendering and materializes them only when a streaming continuation must outlive that borrow.
Approach
Cow<'protocol, [WebUiFragmentRoute]>.SessionCoreandContinuationVmroute levels owned across host calls.mem::takeinstead of recursively cloning them.<outlet />behavior exactly; its latent bug remains isolated in [Bug]: Second<outlet />at the same route level silently renders nothing #515.The scope maps and name sets deliberately remain
String-keyed, and the scope-map pool remains live across suspension steps. An earlier broadCow<str>prototype improved one-step rendering but a real three-boundary session exposed a deterministic regression: 243 to 249 allocations and 25,081 to 29,539 allocated bytes per run. That design was rejected rather than accepting a streaming trade-off.Exact allocation results
Baseline is the current stacked base, #511 at
e908d327. Counts come from the benchmark counting allocator and were deterministic across repeated runs.streaming_resource_bench, scale 1000:The same 14-allocation and 1,749-byte reduction holds at scales 10 and 100. Output remains byte-identical at all scales: 24,114 / 24,134 / 24,152 B.
Nested routes:
//sections/a/sections/a/topics/b/sections/a/topics/b/lessons/cEach route output length and checksum is identical before and after.
Suspension regression checks
The existing benchmark's streaming rows do not encounter a runtime boundary, so they cannot validate state parked across host calls. #517 tracks adding a durable suspending case. This PR was additionally measured with two real
StreamingSessionprobes:Both paths exactly match the base for allocations, allocated bytes, and output. The nested-route probe specifically covers the borrowed-to-owned transition and caught an intermediate recursive clone before the final move-based implementation restored exact parity.
Wall time
No speedup is claimed. Separately built release binaries were run in alternating order, n=9 per build and path, with warm-ups. Every distribution overlaps, so the defensible result is no measurable wall-time regression.
RSS is intentionally omitted from the comparison because #516 demonstrates that the current column is multimodal and dominated by fixture construction. Exact allocator counts are unaffected by that issue.
Validation
cargo test -p microsoft-webui-handler: 457 unit and 36 integration tests passed.cargo xtask check: full workspace gate passed.ContinuationVmto ordinary-render nested outlet path.No public API or behavioral contract changed, so no
DESIGN.mdor developer-doc update is required.Stacked on #511 and targeting
mohamedmansour-reduce-handler-render-allocations. Review #511 first.