Skip to content

Simplify the resource-refresh pass: drop mounted-output snapshots - #1392

Merged
Alexandre Zollinger Chohfi (azchohfi) merged 4 commits into
mainfrom
azchohfi-publish-reactor-idle-wait-theme
Oct 19, 2026
Merged

Alexandre Zollinger Chohfi (azchohfi) merged 4 commits into
mainfrom
azchohfi-publish-reactor-idle-wait-theme

Conversation

@azchohfi

@azchohfi Alexandre Zollinger Chohfi (azchohfi) commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This simplifies the resource-refresh pass added in #1329.

Theme.NotifyResourcesChanged() still refreshes what it was built for: Theme.Ref bindings, ThemeRef resource overrides, and values resolved during Render. It still walks into every memoized subtree, kept list item and unchanged TreeView without remounting anything, so component state, selection and expansion survive.

What's removed is support for brushes a memo factory or FlipView item builder captures eagerly with ThemeRef.Resolve(...). To refresh those, #1329 recorded what every memo layer and FlipView item mounted and diffed against that record. That needed:

  • per-layer snapshot state (KeyedMemoOutput, MoreKeyedMemoOutputs);
  • ordering for nested memos that share a key;
  • legacy-wrapper handling;
  • fresh-control markers;
  • a wrapper-value replay;
  • snapshot cleanup.

That is most of #1329's reconciler code, for a pattern with 0 uses in samples, docs or the agent kit, against about 1,100 uses of Theme.Ref. The styling guide and CHANGELOG now state the contract: a brush resolved eagerly and kept is a fixed value, so bind it with Theme.Ref.

What stays, or is simplified:

  • Keyed memos: during a refresh, a keyed memo re-runs its factory and diffs the output against itself. This reuses the existing path that walks into a memo when a component inside it updated its own state.
  • Transparent wrappers: a keyed memo, or a flyout that wraps its target, whose inner update returns a different control applies its own modifiers with mount semantics. This one rule replaces the fresh-control markers.
  • FlipView: keeps main's per-item record: the element each item mounted and whether it produced a control. That record is the baseline for updates, as the container tag is for a templated ListView, and stops an Empty item from misaligning them. It is cleared when the FlipView unmounts.
  • Hosted-slot unmount fix (UpdateHostedChild / UpdateChildTracked) from Hosts: ReactorHostControl idle wait; Theme.NotifyResourcesChanged() for runtime resource edits #1329 is unchanged.

src/Reactor/Core changes by +32/−348 lines against main.

Linked issue / spec

Follow-up to #1329 (issue #1327).

Test plan

  • Rewrote Hosting_NotifyResourcesChangedRefreshesMemoizedThemeModifiers around the contract. After a resource edit, an ordinary re-render leaves values stale; NotifyResourcesChanged from a background thread then refreshes:

    • a UseMemo'd subtree and a keyed memo;
    • nested memos, including nested memos with the same key, inside a legacy wrapper, or under a flyout's target;
    • ComboBox items with Empty gaps;
    • ListView/GridView items with a kept array, and a LazyVStack mid-scroll;
    • TreeView content;
    • a FlipView with an Empty item;
    • a value resolved in Render.

    Nested components are not remounted (one mount, no cleanups). TreeView expansion and ListView selection survive.

  • New probes for controls replaced on an ordinary render: a flyout whose target changes type is torn down once and its margin and OnMount reach the new target; a popup's always-remounting child keeps exactly one live copy.

  • Mutation checks. Each change made the listed probes fail:

    Change Failing probes
    Remove the refresh condition on the keyed-memo arm 5
    Remove the refresh skip bypass 9
    Remove the transparent-wrapper rule the flyout-modifier and visualizer-wrapper probes
    Ignore the FlipView record the FlipView Empty-gap and changed-in-place probes

| Remove the unmount clear | the held-unmounted-FlipView probe |
| Remove the hosted-slot unmount | the popup one-live-copy probe |

  • --self-test --filter "Hosting_", --filter "DataGrid_" and --filter "AsyncResource.": 0 failures.
  • Full selftest shards: not clean locally. Both crashed with 0xC0000409 inside DataGrid/AsyncResource fixtures, which pass on their own; unmodified main crashed the same way on this machine (3 out of 3 shard-2 runs). CI is authoritative.
  • Release builds of src/Reactor and tests/Reactor.AppTests.Host (--no-restore): 0 errors.
  • dotnet test tests/Reactor.Tests and the docs compile: not run locally. Restore needs packages from deps: Windows NuGet updates #1385 and nuget.org is unreachable from this machine. CI covers both. docs/guide/styling.md mirrors the one plain-prose paragraph added to the template.

Risk / breaking changes

No public API change; every removed member was internal.

Behavior change from #1329, which is unreleased: a brush captured eagerly inside a memo factory or FlipView item builder is no longer refreshed by NotifyResourcesChanged. This is documented.

Theme.NotifyResourcesChanged refreshes Theme.Ref bindings, ThemeRef resource
overrides and values resolved during Render. #1329 also tried to refresh brushes a
memo factory or item builder captured eagerly with ThemeRef.Resolve, by recording
what every memo layer and FlipView item mounted and diffing against that. That
needed per-layer snapshot state, same-key layer ordering, legacy-wrapper handling,
fresh-control markers, a wrapper-value replay and snapshot cleanup - most of the
reconciler change, for a pattern with no uses in samples, docs or the agent kit.

Drop it and document the contract instead: an eagerly captured brush is a fixed
value; bind it with Theme.Ref. A keyed memo now re-runs its factory against itself
during a refresh, through the existing dirty-descendant path.

Also move the hosted-slot unmount fix (UpdateHostedChild / UpdateChildTracked) out;
it fixes a leak on main independent of refresh and goes in its own PR.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build metrics

Artifact sizes for d91e7b1 vs the base branch (fb618d7).

Packages (compressed .nupkg)

Artifact base PR Δ
Microsoft.UI.Reactor.nupkg 1.83 MB 1.83 MB -2.38 KB (-0.13%) ✅
Microsoft.UI.Reactor.Advanced.nupkg 493.3 KB 493.3 KB +6 B (0.00%) ≈
Microsoft.UI.Reactor.Devtools.nupkg 288.1 KB 288.1 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.55 MB 2.55 MB -4.00 KB (-0.15%) ✅
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 788.0 KB 788.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.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🧪 Merged coverage

Coverage for d91e7b1 vs the base branch (fb618d7) — unit + selftest merged.

Metric base PR Δ
Line 86.85% 86.80% -0.05 pp ≈
Branch 77.36% (967/1250) 77.36% (967/1250) 0.00 pp ≈

No coverage change beyond the noise floor. ✅

✅ 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Confirmed reconciliation regressions affect update safety, wrapper styling, and exactly-once teardown.

4 open issues
What changed in this PR

Simplifies Reactor’s resource-refresh implementation from #1329 by removing mounted-output snapshots and narrowing support for eagerly resolved brushes.

Changes:

  • Reuses memo self-diffing and removes snapshot bookkeeping.
  • Removes hosted-child tracking changes intended for a separate PR.
  • Updates selftests and documents the supported resource-refresh contract.
File Description
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​HostIdleAndThemeResourceFixtures.cs Refocuses refresh tests on supported bindings and state preservation.
src/​Reactor/​Core/​V1Protocol/​TemplatedListLifecycle.cs Removes FlipView item snapshots.
src/​Reactor/​Core/​V1Protocol/​Reconciler.TemplatedTree.cs Restores direct tree-content updates.
src/​Reactor/​Core/​V1Protocol/​OverlayLifecycle.cs Removes tracked overlay-content updates.
src/​Reactor/​Core/​V1Protocol/​NavigationHostLifecycle.cs Removes tracked navigation-child updates.
src/​Reactor/​Core/​V1Protocol/​Handlers/​ExpanderHandler.cs Restores ordinary child updates.
src/​Reactor/​Core/​V1Protocol/​CompositeLifecycle.cs Removes tracked composite-child updates.
src/​Reactor/​Core/​Reconciler.Update.cs Reuses memo self-diffing; removes refresh-specific wrapper handling.
src/​Reactor/​Core/​Reconciler.Mount.cs Removes memo snapshot recording.
src/​Reactor/​Core/​Reconciler.cs Removes snapshot state and tracked-update helpers.
src/​Reactor/​Core/​ElementPool.cs Removes snapshot cleanup.
src/​Reactor/​Core/​ChildReconciler.cs Removes tracked child-replacement updates.
docs/​guide/​styling.md Documents eager-resolution limitations.
docs/​_pipeline/​templates/​styling.md.dt Updates the styling guide’s source template.
CHANGELOG.md Clarifies the resource-refresh contract.

Review effort: Balanced

Comment thread src/Reactor/Core/ChildReconciler.cs Outdated
Comment thread src/Reactor/Core/Reconciler.Update.cs
Comment thread src/Reactor/Core/Reconciler.Update.cs
Comment thread src/Reactor/Core/V1Protocol/TemplatedListLifecycle.cs Outdated
…r transparent wrappers

#1329 merged with the hosted-slot unmount fix, so it stays rather than being split
out (removing it would reintroduce double teardown on replacement and the popup/
Expander leaks). The FlipView keeps a per-item 'realized a control' map (no element
snapshot) so an Empty item can't misalign the update. A keyed memo or target-wrapping
flyout whose inner update returns a different control applies its own modifiers with
mount semantics - one rule instead of the fresh-control markers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

An unresolved ordinary-update regression and shared-control lifecycle changes require human review backed by complete Windows CI results.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Fresh view rebuild leaves mutated item text stale

src/​Reactor/​Core/​V1Protocol/​TemplatedListLifecycle.cs:375

Rebuilding the old view loses the baseline for ordinary updates too. With FlipView(items, ..., (item, _) => TextBlock(item.Name)), mutate an item's Name, then re-render with a fresh array containing that item. Both o.BuildItemView(i) and n.BuildItemView(i) now read the new name, so Element.ShallowEquals skips updating the TextBlock and its displayed text stays stale. The previous mounted-element record preserved the old text. Keep a reliable mounted-value baseline for ordinary updates and add this regression test; the presence-only map only preserves item alignment.

4 resolved since last review

Review effort: Balanced

A view rebuilt from the old element reads the items' current values, so an item
changed in place compared equal to itself and stayed stale on an ordinary update.
Restore main's per-item (element, realized) record - the FlipView counterpart of the
element a templated ListView tags on its container - and cover the in-place change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azchohfi

Copy link
Copy Markdown
Collaborator Author

Fixed the "previously missed" FlipView finding in 6bc91d3. The FlipView keeps main's per-item record again: the element each item mounted and whether it realized a control. It diffs against that record, as a templated ListView diffs against the element tagged on its container, so an item changed in place is no longer compared to itself. New probe Replace_FlipViewItemChangedInPlaceUpdates: an item's Name is changed in place and the item is shown again in a fresh array, so the FlipView shows the new text. Mutation check: rebuilding the old view from o fails it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Removed unmount cleanup leaves surviving FlipView item graphs retained on app-held removed controls.

1 open issue

Review effort: Balanced

Comment thread src/Reactor/Core/Reconciler.cs
The record holds item element graphs; an unmounted FlipView an app still holds must
not keep them, and FlipView never reaches pool cleanup. Restore main's clear at the
top of both unmount traversals and cover it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Core reconciliation and lifecycle changes need final human review and confirmation from the full Windows test suites.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@azchohfi
Alexandre Zollinger Chohfi (azchohfi) merged commit 50ea43e into main Oct 19, 2026
46 checks passed
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) deleted the azchohfi-publish-reactor-idle-wait-theme branch October 19, 2026 20:25
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 19, 2026
Hosts: kept both sides. Main's DiagnosticsRootResolver alongside K's
diagnostics-implies-source-map setup; main's passRoot snapshot alongside K's
_renderingMountSite/_renderingFunctionRoot; on a null render, K's
RepublishRetainedRoot runs before main's _renderedRoot bookkeeping (independent).
API index regenerated (UPDATE_API_INDEX=1). K never relied on the secret
redaction #1332 removed: ReactorSource publishes static facts only.

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.

2 participants