You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Replay ancestor Context when virtualized hosts realize rows - #1387
Virtualized hosts (LazyVStack/ItemsRepeater, ItemsView, typed and untyped ListView/GridView, typed TreeView) build rows from WinUI layout callbacks (IElementFactory.GetElement, ContainerContentChanging). Those callbacks run outside the reconciler's tree walk: either after it has finished (first display, scrolling), or in the middle of an unrelated update, when something calls UpdateLayout() synchronously. Rows built there had two problems.
1. Rows couldn't see their ancestors' Context.UseContext returned the default and child-mode UseNavigation<T>() threw.
Fix: each host saves its provider values (a ContextCapture on its ReactorState) whenever it is reconciled. Row building swaps those values in, replacing the live stack rather than pushing on top, so a row never sees whatever subtree happens to be mid-update.
Keeping the saved values fresh. A list counts as out of date when the live providers no longer match the start of its saved values, or when the reconciler's provider generation has moved. The generation goes up whenever any element's provided values change, including additions and removals. Removals can't be detected by the match alone, and a list nested in a scrolled-off row may only be reached in a later update, so it has to rely on the generation.
An out-of-date list declines the "unchanged element" skip and re-saves its values.
Replaying an out-of-date snapshot uses a one-off generation token, so nothing captured from it ever looks current, even across repeated rebuilds. That includes an inner list mounted inside a rebuilt row. A provider change inside a rebuilt row also advances that replay's generation.
The skip path re-saves at the list's own depth. Only the element that registered the snapshot may refresh it, so a decorator sharing the control (Flyout, …) can't overwrite it.
While a row is being rebuilt from saved values, context checks stay on even if those values are empty.
ElementFactory<T> takes its own snapshot when no host snapshot is attached, so a custom handler built on it is also covered.
Pooling or retiring a host empties its saved values, so the factory that shares them stops keeping old provider objects alive.
2. A row built mid-update joined that update's bookkeeping.
If the update was mounting an ErrorBoundary whose child failed, the rollback also tore down those rows (unrelated, still on screen) and cleaned up their effects.
A row that threw was reported to that unrelated boundary instead of showing its own error.
Fix: row building now runs inside a RealizationFrame. While the row is built it:
sets aside the update's boundary rollback lists and depth, its provider- and theme-change depths, the element being reconciled, the stagger state, and the diagnostic owner;
swaps in the saved providers;
picks the theme for ThemeRef brushes, as below;
and restores everything afterwards.
How the theme is picked:
During the update that last reached the list, the row uses the theme that update is applying to the list's subtree, because the native RequestedTheme is only applied after children.
Otherwise it reads the list control's live ActualTheme, so an ancestor theme change while the list is skipped is also picked up.
An element whose own RequestedTheme changed makes the lists below it decline the skip once. A theme generation tracks this, mirroring the provider generation, so a skipped list re-records the pending theme before a later sibling can force layout in the same update.
Related: microsoft/windows-rs#5085, where windows-reactor hits the same synchronous-realization constraint.
Test plan
Unit tests (tests/Reactor.Tests/ContextCaptureUnitTests.cs) for:
the match and swap helpers (IsPrefixOf, Matches, Replace/Restore, including nested replay);
when a snapshot counts as out of date, including the removed provider, a generation change in an earlier update, provider changes during a replay, and a parked grandchild after a second reuse;
Clear, and context checks staying on during an empty replay.
rows built during a failing boundary mount stay alive;
a row that throws during a boundary mount shows its own error and doesn't trigger the boundary;
a list nested in a reused row picks up a provider removal;
a retired host releases the saved values the factory shares;
an ancestor theme flipped while the list is skipped, with no Context providers;
rows built during the update that flips an ancestor theme use the new theme, both when the list changes and when it is unchanged;
an inner list mounted while an out-of-date snapshot is replayed is not treated as current;
a reused row that drops its own .Provide around an unchanged inner list refreshes that list.
Checked against code without the fix. On e3a320f5e, the isolation checks fail:
cleanups=14 of 17 visible rows;
boundary tripped by the row;
n0.15:removed;
entries=1.
The later checks fail on the commit before their fix:
theme: 5d043c3c9 (right=0/27), ef99d20eb (right=0/15) and 4f9856dbb (right=14/25);
replay: 0cea32b0f (declines skip) and 7665a9e60 (entries=1).
dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test (full suite, after merging main): 9,197 ok, 1 failure. The failure is ReactorSource_LiveDialog_SourceKeptSkip_ClosedDialogStopsDescribingItsElement, a new main diagnostics test with no virtualized list that waits two 50 ms renders for a ContentDialog to close. It passes 3/3 when run alone.
dotnet build src/Reactor/Reactor.csproj -c Release: succeeded. dotnet build Reactor.slnx --no-restore -c Release (earlier head): 0 compiler or analyzer errors; only the src/vs-reactor projects failed to restore, because the local proxy blocks nuget.org.
dotnet run --project tools/Reactor.DocPipeline -- compile --no-screenshots: no changes after the merge; the GetElement snippet in architecture-overview.md is current.
Perf (tests/stress_perf/ci/Run-PerfBenchmark.ps1, 3 interleaved runs against the base 20a076b1b, measured at 2cbc3c3b5): every macro workload, the allocation counts and all 17 reconciler micro-benchmarks were within noise. Later commits only add per-row-build field swaps plus one ActualTheme read, and re-save each list's snapshot once after a provider or theme change.
Risk / breaking changes
No public API change. All new types and members are internal.
Behavior changes, both in the CHANGELOG:
Rows now read provided values instead of defaults.
A row that throws while being built during another update now shows its own error instead of triggering an unrelated ErrorBoundary.
Perf: after a provider or RequestedTheme change, each virtualized list is updated once even if unchanged; nothing changes in steady state.
Known follow-ups (not fixed here): two compound edge cases from the final review round. Both need an inner virtualized list, an ancestor RequestedTheme change, synchronous layout forced in the same update, and ThemeRef resource overrides on the inner rows:
an earlier sibling forces the outer list to build a row before the update reaches the list; an inner list mounted in that row records the not-yet-applied old theme as current;
in an app with no Context providers, a reused row's unchanged inner list isn't re-checked for the theme change while the row is being rebuilt.
The fix for both is to record whether a replayed theme is current, so anything derived from an outdated one stays stale until the normal update refreshes it. On main these rows were already resolved against whatever theme happened to be active mid-update.
Not covered: a custom handler that builds rows itself from ContainerContentChanging, without ElementFactory<T>. It gets neither the saved context nor the isolation (noted in the CHANGELOG).
Virtualized hosts (LazyVStack/ItemsRepeater, ItemsView, ListView/GridView,
typed TreeView) build rows from WinUI layout callbacks that run outside the
reconciler traversal, so rows read an empty or unrelated ContextScope:
UseContext returned the default and child-mode UseNavigation threw.
Each host now snapshots the provider stack when it is reconciled, and the
realization callbacks replay that snapshot around the row mount. A host whose
snapshot no longer matches the live providers declines the reference-stable
skip so the snapshot is refreshed before the next realization.
Fixes#1386
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
Artifact sizes for 829a5b5 vs the base branch (5ba98bf).
Packages (compressed .nupkg)
Artifact
base
PR
Δ
Microsoft.UI.Reactor.nupkg
1.90 MB
1.91 MB
+6.03 KB (+0.31%)
⚠️
Microsoft.UI.Reactor.Advanced.nupkg
493.3 KB
493.3 KB
+3 B (0.00%)
≈
Microsoft.UI.Reactor.Devtools.nupkg
287.9 KB
287.9 KB
+8 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor
Artifact
base
PR
Δ
Reactor.Analyzers.dll
410.5 KB
410.5 KB
+0 B (0.00%)
≈
Reactor.dll
2.65 MB
2.66 MB
+7.50 KB (+0.28%)
⚠️
Reactor.Localization.Generator.dll
16.0 KB
16.0 KB
+0 B (0.00%)
≈
Reactor.Wrappers.Abstractions.dll
10.5 KB
10.5 KB
+0 B (0.00%)
≈
Reactor.Wrappers.Generator.dll
99.5 KB
99.5 KB
+0 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor.Advanced
Artifact
base
PR
Δ
Reactor.Advanced.dll
1.00 MB
1.00 MB
+0 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor.Devtools
Artifact
base
PR
Δ
Microsoft.UI.Reactor.Devtools.dll
787.0 KB
787.0 KB
+0 B (0.00%)
≈
✅ smaller / ⚠️ larger / ≈ within noise. Sizes come from a Release dotnet pack on the CI runner: packages are the compressed .nupkg download size, assemblies the uncompressed DLL inside it. workflow run.
Coverage for 829a5b5 vs the base branch (5ba98bf) — unit + selftest merged.
Metric
base
PR
Δ
Line
87.34%
87.39%
+0.05 pp
≈
Branch
76.77% (978/1274)
76.57% (977/1276)
-0.20 pp
⚠️
✅ higher / ⚠️ lower / ≈ within noise. Δ is in percentage points; coverage is unit + selftest merged (Debug x64) on the CI runner. Cobertura reports attached to the workflow run as artifacts.
- Detect a provider removed above a skipped virtualized host: count
non-skip Updates whose own ContextValues changed and treat any host
snapshot (and the context skip gate) below them as stale. Also fixes a
reference-stable consumer keeping a removed provider's value when the
last provider is dropped.
- Re-snapshot on the element skip arm so a host sharing its control with
a decorator (Flyout, ...) keeps a .Provide() on the list element itself.
- ElementFactory<T> captures its own snapshot when no host capture is
attached, so out-of-tree handlers built on it are covered.
- Refresh reuses the snapshot array when nothing changed.
- Unit tests for ContextScope/ContextCapture primitives; selftests for
provider removal, decorator-wrapped list, typed GridView and untyped
ListView/GridView. CHANGELOG entry.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
- Refresh a virtualized host's context snapshot only from the element that
registered it, so a reference-stable decorator (Flyout, ...) sharing the
host's control cannot overwrite it at its own shallower depth.
- ElementFactory<T> drops its fallback snapshot once a host capture is
attached, so it no longer pins mount-time provider values.
- Regenerate docs/guide/architecture-overview.md (GetElement snippet).
- Selftest for a skipped whole decorator; list fixtures wait for
realization (TreeView realizes over more than one layout wave).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
Recycled rows retain removed provider values after context replay
src/Reactor/Core/Reconciler.cs:1906
A recycled row can retain the last provider's removed value. ElementFactory.RecycleElement removes parked rows from _mountedElements without unmounting them, so the provider-removal update does not refresh those rows. When GetElement later reuses one under the now-empty capture, _providerChangeDepth is zero and this predicate disables context checking. A shallow-equal row root, such as a Border around a reused consumer element, then skips and keeps the removed value instead of reading the default. Keep context checks enabled during captured-context replay even for empty snapshots, with balanced restoration for nested calls and exceptions. Add a regression that recycles a row, removes its only provider, and realizes the same row again.
Keep context skip checks enabled while a captured snapshot is replayed,
even when the snapshot is empty because the host's last provider was
removed. A recycled row reused for the same item under that empty
snapshot otherwise shallow-skips its reference-stable consumer and keeps
the removed value. Gated on the scope having ever seen a provider, so
provider-free apps pay nothing during realization.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
A recycled row can contain a nested host whose capture still includes a removed ancestor provider. On later GetElement, the outer factory replays its updated snapshot, but _providerChangeDepth is already zero. For a shallow-equal Border(stableNestedList) with no realized context consumers, the shorter replay remains a prefix of the nested capture, so this check allows the Border to skip. The nested host never refreshes, and rows it realizes later read the removed value. Carry removal invalidation into recycled-row replay, and add a nested-host variant of Factory_RecycledRowAfterLastProviderRemoved. This occurs at the element-level skip, without a component memo boundary.
Clear factory host capture when retiring nested host state
src/Reactor/Core/Reconciler.cs:1259
Dropping this reference leaves the same snapshot reachable through ElementFactory<T>._hostCapture. For example, retiring an outer repeater row containing a nested virtualized host detaches its Reactor state but leaves the native subtree and nested ItemTemplate/factory alive. The factory can therefore retain obsolete ancestor provider objects for the outer list's lifetime. Empty the shared capture before dropping the state reference so factory aliases also release those values, and add a nested-host retirement regression.
Rows realized by WinUI layout can run re-entrantly inside an unrelated
Reactor update (any synchronous UpdateLayout). They used to join that
update's bookkeeping. Realization now runs in a RealizationFrame that sets
aside the ErrorBoundary rollback journal/roots and boundary depth, the
provider-change depth, the reconciling element and stagger scope, replays
the host's captured providers and ambient theme, and makes the owning
ReactorHost active for window-scoped hooks.
- A failing boundary child no longer tears down live rows in another list.
- A row that throws gets its own in-tree fallback instead of tripping an
unrelated boundary.
- Rows realized outside a render resolve their own window host.
Round 3 findings:
- Host snapshots record a provider generation bumped on every provider
change, so a nested host in a recycled row that comes back in a later
pass still re-snapshots after a removed provider.
- Retiring/pooling a host empties its capture so the aliasing
ElementFactory no longer keeps old provider values alive.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
- Read the ambient theme for realized rows live from the host control's
ActualTheme instead of capturing it at reconcile time. A RequestedTheme
change on an ancestor while the host is skipped no longer leaves rows
resolving ThemeRef overrides against the old theme (works with no
Context providers at all).
- Use an epsilon comparison in the nested-host fixture's selector.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
Refreshing the capture does not refresh already-realized rows in either untyped ListView or GridView. With a stable Items array, changing or removing an ancestor provider now declines the host's skip and refreshes its snapshot, but both handlers leave ItemsSource unchanged and never reconcile existing rows (ListViewHandler.cs:187–192,222; GridViewHandler.cs:165–170). Visible consumers therefore retain the old provided value while newly realized rows read the new value. Reconcile realized containers when their context changes, rather than only updating the snapshot for future realization. Add provider-change and provider-removal regressions for both untyped hosts with a stable Items array.
Use the pass's pending subtree theme for rows realized re-entrantly in
the same reconcile pass that last reached their host. The native
RequestedTheme modifier is applied only after children are dispatched,
so the host's ActualTheme still shows the previous theme then. Hosts
record the pass id and effective theme at mount, update and skip;
outside that pass the live ActualTheme is still used, so a host skipped
while an ancestor theme changes stays correct.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Nested hosts created during stale context replay can be marked current and retain removed provider values.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Nested snapshots incorrectly mark replayed provider values as current
src/Reactor/Core/ContextScope.cs:172
A nested host can capture removed providers and incorrectly be marked current here. After a provider is removed, an earlier sibling can synchronously realize rows before the outer list refreshes its capture. Those rows replay the old values, but a nested list mounted inside them records the new ProviderGeneration. If its initially realized rows do not consume context, the subsequent traversal can skip the unchanged row subtree: the nested capture's generation matches, and the empty live stack passes IsPrefixOf. Later nested rows then read the removed provider. Preserve the replay source's generation when taking nested snapshots, rather than marking old values current. Add a regression that realizes nested hosts before the outer host refreshes, then scrolls the nested list to context-consuming rows.
A host snapshot taken while an outdated snapshot is being replayed (a
nested host mounted inside a row realized before the traversal refreshed
the outer host) now records the replayed snapshot's provider generation
instead of the current one, so it is not marked current while holding a
removed provider. ContextScope tracks the generation of each replay;
captures record and compare against SnapshotGeneration.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
A provider change inside a replayed subtree (a reused row dropping its
own .Provide around a reference-stable nested host) now advances the
replay's generation: to the new live generation if the replay was
current, or to a unique never-current value if it was already stale.
Nested snapshots taken under the replay before the change are stale, so
the nested host declines the skip and re-snapshots.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Same-pass theme handling for skipped lists and generation tracking across repeated replays still have correctness gaps.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Prevent repeated capture replay from restoring invalidated providers
src/Reactor/Core/ContextScope.cs:168
Replaying the same capture again can undo the new provider invalidation. An outer factory can retain generation 0 while a reused row drops .Provide: the first reuse advances the generation to 1 and refreshes an inner list to empty/gen1, but a parked grandchild list still holds the removed provider/gen0. A second reuse replays gen0 here, and refreshing the inner list stamps its empty capture with gen0 again. When that inner list realizes the parked grandchild, IsStale returns false: the generations match and the empty stack is a prefix of the old capture. Later rows can therefore read the removed provider. Preserve invalidation across repeated replays, for example by assigning stale replay invocations distinct non-live generation tokens. Extend the regression with a second recycle/reuse and a parked grandchild.
Propagate pending theme through child skips during same-pass realization
src/Reactor/Core/Reconciler.cs:2079
The pending-theme fix misses child-level skips. ChildReconciler.UpdateCommonChild (lines 221–248) can return without either capture-refresh helper. With no Context providers, keep a list reference-stable, change its ancestor's RequestedTheme, and scroll/force layout from a later sibling's effect in the same update. The capture still has the previous pass ID, so this reads the old ActualTheme before the ancestor's modifier is applied. Newly realized rows then retain incorrectly resolved ThemeRef resource overrides. The current in-pass fixture changes the list's items, while the stable-list fixture scrolls after the update; neither covers this combination. Propagate the pending theme through child/container skips, respecting host-local overrides, and add a stable-list, same-pass realization regression.
- Every replay of an outdated host snapshot gets a fresh generation token
that no live generation or other replay can equal, so snapshots taken
from it are never treated as current - including across repeated
replays of the same outdated snapshot (parked grandchild after a second
reuse).
- Track RequestedTheme changes like provider changes: a non-skip Update
whose own RequestedTheme changed bumps a theme generation and keeps the
context skip check active below it. A virtualized host whose recorded
theme generation is behind declines the skip once, so its Update
records the pending subtree theme before a later sibling forces layout
in the same pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
Resolve the CHANGELOG conflict by keeping both Fixed entries.
Main's #1399 resolves window hooks from the reconciler-supplied owner,
which every component render receives (including virtualized rows), so
drop this branch's ActiveHostInternal swap in RealizationFrame, the
ReactorHost.OwnerHost wiring and its fixture; main's
WindowHookOwnerFixtures.VirtualizedRow covers that case. RealizationFrame
now also sets aside main's per-render diagnostic owner, so a row built
re-entrantly is not attributed to the component being rendered.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6963d93-8fc8-4956-8c83-88b394debeee
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
ready-for-reviewAgent work and technical checks complete; awaiting review, not approval or merge
2 participants
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.
Summary
Virtualized hosts (
LazyVStack/ItemsRepeater,ItemsView, typed and untypedListView/GridView, typedTreeView) build rows from WinUI layout callbacks (IElementFactory.GetElement,ContainerContentChanging). Those callbacks run outside the reconciler's tree walk: either after it has finished (first display, scrolling), or in the middle of an unrelated update, when something callsUpdateLayout()synchronously. Rows built there had two problems.1. Rows couldn't see their ancestors'
Context.UseContextreturned the default and child-modeUseNavigation<T>()threw.ContextCaptureon itsReactorState) whenever it is reconciled. Row building swaps those values in, replacing the live stack rather than pushing on top, so a row never sees whatever subtree happens to be mid-update.Flyout, …) can't overwrite it.ElementFactory<T>takes its own snapshot when no host snapshot is attached, so a custom handler built on it is also covered.2. A row built mid-update joined that update's bookkeeping.
ErrorBoundarywhose child failed, the rollback also tore down those rows (unrelated, still on screen) and cleaned up their effects.Fix: row building now runs inside a
RealizationFrame. While the row is built it:ThemeRefbrushes, as below;and restores everything afterwards.
How the theme is picked:
RequestedThemeis only applied after children.ActualTheme, so an ancestor theme change while the list is skipped is also picked up.RequestedThemechanged makes the lists below it decline the skip once. A theme generation tracks this, mirroring the provider generation, so a skipped list re-records the pending theme before a later sibling can force layout in the same update.Linked issue / spec
Fixes #1386
Related: microsoft/windows-rs#5085, where windows-reactor hits the same synchronous-realization constraint.
Test plan
Unit tests (
tests/Reactor.Tests/ContextCaptureUnitTests.cs) for:IsPrefixOf,Matches,Replace/Restore, including nested replay);Clear, and context checks staying on during an empty replay.Context selftests (
VirtualizedRowContextFixtures.cs):Flyout, both updated and unchanged;UseNavigation;ListView/GridView/ItemsView/TreeViewand untypedListView/GridView.Isolation selftests (
RealizationIsolationFixtures.cs):.Providearound an unchanged inner list refreshes that list.Checked against code without the fix. On
e3a320f5e, the isolation checks fail:cleanups=14of 17 visible rows;n0.15:removed;entries=1.The later checks fail on the commit before their fix:
5d043c3c9(right=0/27),ef99d20eb(right=0/15) and4f9856dbb(right=14/25);0cea32b0f(declines skip) and7665a9e60(entries=1).dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test(full suite, after mergingmain): 9,197 ok, 1 failure. The failure isReactorSource_LiveDialog_SourceKeptSkip_ClosedDialogStopsDescribingItsElement, a newmaindiagnostics test with no virtualized list that waits two 50 ms renders for aContentDialogto close. It passes 3/3 when run alone.dotnet test tests/Reactor.Tests --no-build -p:Platform=x64(after mergingmain): passed, 15,109 succeeded, 0 failed.dotnet build src/Reactor/Reactor.csproj -c Release: succeeded.dotnet build Reactor.slnx --no-restore -c Release(earlier head): 0 compiler or analyzer errors; only thesrc/vs-reactorprojects failed to restore, because the local proxy blocks nuget.org.dotnet run --project tools/Reactor.DocPipeline -- compile --no-screenshots: no changes after the merge; theGetElementsnippet inarchitecture-overview.mdis current.Perf (
tests/stress_perf/ci/Run-PerfBenchmark.ps1, 3 interleaved runs against the base20a076b1b, measured at2cbc3c3b5): every macro workload, the allocation counts and all 17 reconciler micro-benchmarks were within noise. Later commits only add per-row-build field swaps plus oneActualThemeread, and re-save each list's snapshot once after a provider or theme change.Risk / breaking changes
No public API change. All new types and members are
internal.Behavior changes, both in the CHANGELOG:
ErrorBoundary.Perf: after a provider or
RequestedThemechange, each virtualized list is updated once even if unchanged; nothing changes in steady state.Window hooks in rows: handled by
main's Resolve window hooks from the reconciler-supplied owner, not ActiveHostInternal #1399, where window hooks read the reconciler-supplied owner that every component render gets. This PR doesn't touchReactorApp.ActiveHostInternal.Known follow-ups (not fixed here): two compound edge cases from the final review round. Both need an inner virtualized list, an ancestor
RequestedThemechange, synchronous layout forced in the same update, andThemeRefresource overrides on the inner rows:The fix for both is to record whether a replayed theme is current, so anything derived from an outdated one stays stale until the normal update refreshes it. On
mainthese rows were already resolved against whatever theme happened to be active mid-update.Not covered: a custom handler that builds rows itself from
ContainerContentChanging, withoutElementFactory<T>. It gets neither the saved context nor the isolation (noted in the CHANGELOG).