Skip to content

Fix keyed child mapping while exit transitions are retained - #1429

Merged
Alexandre Zollinger Chohfi (azchohfi) merged 8 commits into
microsoft:mainfrom
bkudiess:bkudiess-reactor-keyed-exit-crash
Oct 29, 2026
Merged

Alexandre Zollinger Chohfi (azchohfi) merged 8 commits into
microsoft:mainfrom
bkudiess:bkudiess-reactor-keyed-exit-crash

Conversation

@bkudiess

@bkudiess Barbara Kudiess (bkudiess) commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Keep keyed children paired with their own native controls while removed siblings remain for exit transitions, preserving content, identity, order, and Grid placement through updates, inserts, removals, and LIS reorders. Also finish orphan-view cleanup when WinUI clears an unlinked list container's item but leaves its template view mounted.

The logical-to-physical map skips retained exits and preserves absent transparent Memo slots, including unkeyed Memos in mixed-key lists. Exit detection is collection-local; an exit in another panel sharing the reconciler cannot misalign updates. Insertion anchors resolve to the next realized sibling, and null mounts do not advance physical insertion positions. Orphan cleanup preserves exit visuals while clearing lifecycle ownership, rejecting stale state writes and making later recycle idempotent.

Linked issue / spec

Test plan

  • Native fixtures cover leading/trailing exits, pure removal, suffix insertion, middle reorder, same-key type replacement/Grid placement, dirty-child updates, and absent Memo slots. Empty-Memo cases cover no exits, local exits, unrelated-panel exits, and both; mixed-key cases assert both identity and updated content for [a, emptyMemo, b] reordered to [b, emptyMemo, a].
  • Deterministic cleared-item orphan fixture covers cleanup, rejected stale writes, cleared tag, preserved visual identity, and repeat-call idempotence; original churn/move fixtures remain intact.
  • Regression oracles fail against broken code: the new mixed-key cases produced eight failed content/identity assertions before the fix and pass afterward. Earlier keyed-exit, dirty-child, and orphan regressions were also verified against broken behavior.
  • Integrated main through 7f46468f3cb1307c7fbf108a6a575849a65084c9, preserving both changelog entries when resolving the merge conflict.

Local validation of the merged changes at d18edbc8 (the subsequent cef684f7 merge adds only main's perf-workflow path update):

Exact command Result
dotnet restore Reactor.slnx -v:q Passed; existing NU1902/NU1903 warnings for test dependency System.Security.Cryptography.Pkcs 9.0.6.
dotnet build tests\Reactor.AppTests.Host --no-restore -c Release -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false -v:q Passed, 0 warnings/errors.
dotnet run --project tests\Reactor.AppTests.Host --no-build -c Release -p:Platform=x64 -- --self-test --filter 'Transition_' Passed: 12 fixtures, 0 failures, including all keyed and mixed-key empty-Memo cases.
dotnet run --project tests\Reactor.AppTests.Host --no-build -c Release -p:Platform=x64 -- --self-test --filter 'KeyedWrapper_' Passed: 6 fixtures, 0 failures.
dotnet run --project tests\Reactor.AppTests.Host --no-build -c Release -p:Platform=x64 -- --self-test --filter 'ConditionalChildSlot_' Passed: 15 fixtures, 0 failures.
dotnet build tests\Reactor.Tests --no-restore -c Release -p:Platform=x64 -p:SkipSignaturesGen=true -m:1 -nodereuse:false -v:q Passed; existing NU1902/NU1903 warnings.
dotnet test tests\Reactor.Tests --no-build -c Release -p:Platform=x64 --filter-class '*ChildReconciler*' Passed: 117 tests, 0 failures/skips.
dotnet run --project tools\Reactor.DocPipeline -- compile --topic reconciliation --no-screenshots Passed; committed both regenerated source-backed child-diff snippets in the reconciliation guide.
dotnet build Reactor.slnx --no-restore -c Release -v:q Failed locally with MSB3030 copying missing copilot-cli\1.0.98\win32-arm64\plugins\github-copilot-browser\scripts\nativeMessagingRegistration.mjs in untouched demo-script-tool; no sample workaround or suppression included.

Current-head remote CI at cef684f7298b0cb1e26f958abf83f58e04bfe5d8 is green: 39 successful checks, 3 intentional path-filtered skips, no failures or pending checks. CI passed Release solution build, units, integration, both native/packaged/AOT/E2E shards, docs build and snippet gate, fuzz smoke, and AOT trim proof. Coverage, packaging, bootstrap, Node projects, and build metrics also passed. Skips are Devtools trim mstat, Audit ledger gate, and Docs tier-drift. The regenerated reconciliation guide clears the prior Docs build failure.

Earlier local evidence: full units at the initial head passed 15,122 with 64 skipped. A later full local run had a single high-contrast-desktop-dependent theme test failure; it was not weakened, and remote Unit Tests passed. An isolated preview.12 backport reproduced the original hard casts and passed native ARM64 transition fixtures after correction using architecture-neutral local.4 packages; the consumer reported survival of the original reconnect sequence with matching payload hashes. Those packages, backport sources, feed pins, and consumer changes are not part of this PR, and local.4 does not include subsequent readiness fixes.

Risk / breaking changes

No public API change. Keyed reconciliation is a hot path: ordinary count-aligned, non-exiting collections retain the allocation-free prefix/suffix path. Sparse or retained-exit collections rent a logical-to-physical map and sibling-key dictionary, returned in finally; survivor positions count actual immediate versus deferred removals rather than trusting an exiting duplicate key. Null-content orphan cleanup runs only after the caller establishes that the container is unlinked, preserves its visuals, and clears ownership before later recycle/remount.

Keep logical child identity separate from physical collection positions across keyed prefix/suffix updates, removals, insertions and LIS reorders. Add native regressions for in-flight exits and Grid placement.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep end-relative no-exit matching for transparent wrappers that mount no control. Add a reused self-triggered keyed child regression. Fix the current-head selftest/coverage failure by unmounting a tagged item view after WinUI clears its unlinked container's Content; preserve delete visuals and prove cleanup and stale-state rejection deterministically.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve the additive CHANGELOG conflict by retaining both the keyed-exit correction and main's Jump List Native AOT entry. Preserve the auto-merged native/packaged Jump List registrations alongside all keyed-exit and orphan-view regressions.

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.

🟡 Changes recommended

The reconciler-wide exit flag can incorrectly enable mapping for a collection with no exiting controls, skipping or misapplying suffix updates and leaving controls stale.

1 open issue
What changed in this PR

This PR extends the earlier child-reconciliation fixes to preserve keyed controls while sibling exit transitions remain active. It also completes orphaned list-row cleanup when WinUI has already cleared the container’s item.

Changes:

  • Map logical keyed children to physical controls during retained exits.
  • Unmount cleared-item orphan views while preserving their transition visuals.
  • Add and register seven native regression fixtures, and document the fixes.
File Description
tests/​Reactor.AppTests.Host/​SelfTest/​SelfTestFixtureRegistry.cs Registers the seven new fixtures.
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​TransitionTests.cs Covers keyed identity, ordering, placement, empty Memo and dirty-child updates.
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​TemplatedListOrphanContainerFixtures.cs Verifies cleared-item cleanup, rejected stale writes and idempotence.
src/​Reactor/​Core/​Reconciler.Update.cs Handles orphaned containers whose item is already null.
src/​Reactor/​Core/​ChildReconciler.cs Adds retained-exit index mapping for keyed reconciliation.
CHANGELOG.md Documents keyed-exit and orphan-cleanup fixes.

Review effort: Balanced

Comment thread src/Reactor/Core/ChildReconciler.cs Resolved
Preserve null-mounted logical slots and resolve insertion anchors against the next realized sibling. Scope exit detection to each collection and cover local and unrelated-panel exits with exact identity and order assertions.

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

Copy link
Copy Markdown
Collaborator

The Docs build check is also failing because regeneration changes docs/guide/reconciliation.md. Please regenerate the affected guide and commit the output alongside the fix.

Reserve keyed siblings' controls while mapping mixed-key children, cover native reorder and insertion with local and unrelated exits, and regenerate the reconciliation guide. Preserve both changelog entries when integrating main.

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

Copy link
Copy Markdown
Contributor Author

The Docs build check is also failing because regeneration changes docs/guide/reconciliation.md.

Fixed in d18edbc: regenerated the reconciliation guide with dotnet run --project tools\Reactor.DocPipeline -- compile --topic reconciliation --no-screenshots (passed), including both copies of the source-backed child-diff snippet.

Integrate the perf-library workflow path update from main without changing reconciliation code.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. A focused benchmark shows the new native child scans in keyed reconciliation cost about 0.5-1.5 us and 100 B per child per render when an exit is in flight or a child mounts nothing. I'll follow up separately with a perf fix and benchmarks.

@azchohfi
Alexandre Zollinger Chohfi (azchohfi) merged commit 90c9959 into microsoft:main Oct 29, 2026
42 checks passed
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 29, 2026
…1445)

* Avoid per-child control reads in keyed mapping around retained exits

#1429 made the keyed pass read every child of a keyed panel whenever any exit
was in flight on the reconciler, and read each child twice more to build the
logical-to-physical map when a panel held an exit or a null-mounted Memo. Each
UIElementCollection read is a WinRT call that allocates ~52 B, so a 500-child
keyed panel paid +93% to +189% allocation per render.

- Detect an exit in a collection by probing the collection once per in-flight
  exit (IndexOf) instead of reading every child.
- Resolve the keyed map with a forward cursor on demand: a skipped child never
  advances it, each physical child is read at most once, and the control it
  claims is handed to the update so it is not read again.
- Add KX1-KX4 keyed-exit micro benches to the control-model catalog (27 benches).

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

* Address pr-review findings

- Cache the control each keyed-map slot claims so suffix updates reuse the
  cursor's read instead of reading every suffix child again.
- Decide the identity map lazily: an all-skip pass never probes for exits.
- Seed ProbeEachChild dirty checks with the mapped control.
- Raise ExitProbeLimit to 64 with its cost rationale.
- Cover suffix read counts, both sides of the probe limit, and the context /
  callback skip arms under a non-identity map.
- Correct the KX bench header (rows update, they do not skip) and narrow the
  CHANGELOG wording.

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

---------

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.

3 participants