Repository navigation
fix: Stop shared dependency objects retaining discarded elements and their DataContext (6.8 port of #25102) - #25839
Conversation
f9e8686 to
2d8b6f9
Compare
|
Force-pushed a fix to commit 4: it removed |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@MartinZikmund can you help follow up on this PR please |
|
🤖 Your WebAssembly Sample App stage site is ready! Visit it here: |
|
The build 242669 did not find any UI Test snapshots differences. |
MartinZikmund
left a comment
There was a problem hiding this comment.
Reviewed by reading the code against this PR's head, servicing/6.8 and the WinUI C++ sources. I haven't built or run it.
I'd hold this until #25102 settles, since the port should follow whatever it ends up doing:
- The base has moved.
servicing/6.8already has the #26123 backport (5a5cce0 and follow-ups), and six files now conflict, includingDependencyObjectStore.Binder.csandToolTipService.cs. "Same code as master" in the description is no longer accurate. - Dead-parent rule. This port carries the same "a collected parent counts as a second parent" rule that conflicts with #26123. See my review on #25102, including the stale DataContext to handle when a collected parent's slot is reused.
The weak inherited storage in commit 5 looks right. The readers I traced in DependencyPropertyDetails unwrap the value, ModifiedValue stays strong, and the wrapper's handle is disposed when the value is unset.
An open question for servicing, rather than a defect: is this acceptable risk for a 6.8 fix? By the PR's own numbers, WebAssembly DataContext changes cost +2% to +8%. Each affected object gets a finalizable handle, public DataContext on non-UIElements can read null after a GC, and the shared-flyout leak remains.
NativeRenderTransformAdapter subscribed strongly to Transform.Changed and only unsubscribed when the element's RenderTransform was replaced, so a transform shared through a Style setter accumulated one handler per element ever styled with it, keeping every discarded element (and its inherited DataContext) alive. The adapter now registers through a weak registration on the transform, so the transform never roots its subscribers. unoplatform#25099
Transforms are multi-parent shareable, as in WinUI, so one transform can outlive the brushes, geometries and TransformGroups that use it (a resource referenced from a template, for instance). Each of them still subscribed to the strong Transform.Changed event, so the shared transform kept every discarded one alive, and through a geometry's GeometryChanged handler also the Path it draws or the element it clips. WinUI tracks those parents with non-owning pointers. They now go through RegisterChanged and hold the registration, like the render transform adapter, Popup and the projection owner. With no strong subscriber left, the strong Changed events of Transform and Projection are removed so the leak cannot come back through them. TransformGroup now also handles the Reset that Children.Clear() raises without the removed items, so it lets go of the cleared children. The 6.x-only native partials (Transform.Android.cs, Transform.Apple.cs) raised the removed Changed event too; they now notify through the weak handlers, as NotifyChanged does.
A DependencyObject that isn't a UIElement, used as a property value, keeps the DataContext its element pushes into it, so bindings added or resumed on it later resolve against it. That value was held strongly, so a long-lived shared value (a style setter value, a static brush, a TransitionCollection or an InputScope) kept the DataContext of the last element it was applied to, and with it that element's page, alive after navigation. Hold it weakly, as WinUI does: these objects have no DataContext there, their bindings resolve through a non-owning inheritance-context mentor, and the DataContext belongs to the mentor. Master holds its equivalent cache weakly. 6.x stores that DataContext as a property value at Inheritance precedence, so its DependencyPropertyDetails now keeps that value in one weak handle per object, retargeted in place, and every reader unwraps it. A DataContext set on the object itself, and one that has been animated, stay strong. unoplatform#25099
A shareable value whose single parent was collected without unassociating takes the next owner as its single parent again, but kept the DataContext the dead parent had pushed into it. On 6.x an owner pushes its DataContext into a new value only when the assignment changes the property's precedence, so after a same-precedence replacement the value's bindings kept resolving against the old view model whenever that view model was still alive, and the weak inherited DataContext does not help then. WinUI has no such state: a destroyed parent removes itself from its values (CDependencyObject::ReleaseImpl -> ResetReferencesFromChildren), which takes the inheritance context with it. When the expired slot is reused, clear the inherited DataContext unless it already is the new owner's (a precedence-changing assignment has pushed it by then), and leave a value with its own parent alone, like ApplyChildrenBindable. The reused value then ends where a value used for the first time would. The dead parent never released its bindable slot either, so it kept counting as a holder and the guarded clear on release, which waits for the last holder, never fired again for that value: the next owner's DataContext stayed behind once it let go. Give that slot back when the expired association is freed, here and in the ALC sweep. unoplatform#25099
2d8b6f9 to
97a2f17
Compare
|
I rebased onto current
|
|
Tick the box to add this pull request to the merge queue (same as
|
…alue (port) Port of the review fix in unoplatform#25839 (commit 97a2f17), on top of this build's port fd40bf9. On 6.x an owner pushes its DataContext into a new value (TryApplyDataContextOnPrecedenceChange) before the value is associated with it, and only when the assignment changes the property's precedence. The plain clear fd40bf9 added where a collected parent's slot is reused therefore wiped the DataContext a precedence-changing assignment had just pushed. The clear now keeps the inherited DataContext when it already is the new owner's, so a reused value ends where a value used for the first time would: the owner's DataContext after a precedence change, none after a same-precedence replacement.
Fix for 7546379, the port of unoplatform#25839's review fix (97a2f17), which upstream gets in the same form. Where a collected parent's slot is reused, the value now compares its DataContext with the new owner's, read with GetValue(DependencyProperty). That overload checks the property's owner when ValidatePropertyOwnerOnReadWrite is on (Debug builds, or any build with a debugger attached). A POCO owner such as TextHighlighter keeps its values in an attached store, whose DataContext property is owned by AttachedDependencyObject while the store's type is the POCO's, so the check threw "The Dependency Property [DataContext] is owned by [AttachedDependencyObject] and cannot be used on [TextHighlighter]". TextBlock hits it on every draw with a selection: it creates a TextHighlighter for the selection brush, whose previous highlighter has been collected. Both reads now go through the stores' DataContext details, as SetMentoredChild and RepropagateMentoredChildrenDataContext already do.
The expired-slot step in AssociateParent (previous commit) and the guarded release clear in UpdateChildBindable (unoplatform#26142) compare a value's DataContext with its owner's, read with GetValue(DependencyProperty). That overload checks the property's owner when ValidatePropertyOwnerOnReadWrite is on (Debug builds, or any build with a debugger attached). A POCO owner such as TextHighlighter keeps its values in an attached store, whose DataContext property is owned by AttachedDependencyObject while the store's type is the POCO's, so the check threw "The Dependency Property [DataContext] is owned by [AttachedDependencyObject] and cannot be used on [TextHighlighter]". TextBlock hits the first one on every draw with a selection: it creates a TextHighlighter for its selection brush, whose previous highlighter has been collected. The second one fires when a highlighter's brush is replaced. All these reads now go through the stores' DataContext details, as SetMentoredChild and RepropagateMentoredChildrenDataContext already do.
|
|
Takes Uno's servicing/6.8 from 6951c2c up to 5254ace (42 pull requests). Conflicts, by area: IME and TextBox (unoplatform#25984, backport of unoplatform#25804; unoplatform#25952, backport of unoplatform#25619; unoplatform#25976, backport of unoplatform#25818) - Takes upstream's composition design (unoplatform#25804): the browser's input is the source of truth for the text, compositions included. This drops the composition parts of the unoplatform#24502 port (90746b8): its TypeScript composition handling, the StartIndex/TextChangePending event arguments, the IsReadOnly forwarding to the hidden input, and its Given_TextBox.BrowserSoftKeyboard tests. - Keeps the non-IME browser fixes on top of it, in the form their master pull requests have on unoplatform#25804: Enter raised once per press and tracked by key code (unoplatform#25174, with Given_TextBox.BrowserEnterKey from master in place of the Enter tests that lived in the dropped file), the iOS refocus on tap (unoplatform#25185), pastes without text and isIOS from BrowserInputHelper. - Moves the unoplatform#26005 MaxLength fixes onto unoplatform#25804's native input path, as master has them: a refused browser edit keeps the TextBox's selection, TextBoxView.UpdateTextFromNative reports whether the edit went in, and the IME keeps its range on the applied text (master be85699, which d226897 had ported onto the unoplatform#24502 IME). Adds that commit's browser and platform-applied IME tests. - TextBox.pointers.skia.cs: upstream's file (unoplatform#25619 iOS touch editing, unoplatform#25818, unoplatform#25804), with the unoplatform#25126 port (the hold acts when it starts, a touch right-tap keeps the caret) in the form master's unoplatform#25126 branch has it on unoplatform#25619, including focusing before the iOS hold timer acts and its iOS not-focusable test. Pointers (unoplatform#25976, backport of unoplatform#25818) - Tapped and RightTapped are raised once the PointerReleased bubbled (unoplatform#25818), with the unoplatform#25732 port's window coordinates (2-argument routed event args, RawPosition for ContextRequested), as master's unoplatform#25732 branch has them. - The release wrapper of unoplatform#25818 keeps the unoplatform#25126 port's StopContextMenuTimer at the start of the release. DependencyObject (unoplatform#26142, backport of unoplatform#26123) - Takes upstream, then the code of unoplatform#25839 (97a2f17), which replaces this branch's unoplatform#25102 ports (800247f, be41da6, fd40bf9, 7546379). DependencyObjectStore.Binder.cs, .alc.cs, Application.Alc.cs, ResourceDictionary.alc.cs and their tests now match unoplatform#25839; this drops 800247f's own SingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean test, which unoplatform#25839 doesn't have. Imaging (unoplatform#25597, unoplatform#25915, unoplatform#25911, unoplatform#25897) - Keeps this branch's side: it has the later forms of these fixes (unoplatform#25124, unoplatform#25559, unoplatform#25633 and unoplatform#26370 ports), where upstream has the 6.8-only unoplatform#25597 design and earlier snapshots. Upstream's shared download, IgnoreImageCache and failed-load fixes are covered by SharedImageDecode. Adds upstream's When_Source_Outlives_Brushes test. ToolTip (unoplatform#26098, backport of unoplatform#25137 and unoplatform#26096) - Keeps this branch's unoplatform#25137 port and adds unoplatform#26096 in master's form: the service releases only a ToolTip it opened, and leaves one the app opened during the show delay to the app. Keeps both sides' tests. Flyout (unoplatform#26062, backport of unoplatform#26060) - This branch had the same commits already; keeps the "Keep non-resizable flyouts in window" port and its tests. Also removes a helper pair in Given_AccessibleTextBox that both sides added.
Brings in the hotfix for the DataContext reads of an attached owner. DependencyObjectStore.Binder.cs and Given_UIElement.DataContext.cs take the form of unoplatform#25839 at b4bacab, which also covers the release clear that servicing/6.8 added in UpdateChildBindable and adds SingleParentNonFE_Released_By_Owner_With_Attached_Store.
GitHub Issue: Related to #25099
6.8 port of #25102 to
servicing/6.8, in sync with its current head (0efaf28), which was reworked on top of #26123. That PR targetsmasterand is still under review; #25099 stays open until it merges. If the review there changes the fix again, this port will follow.PR Type:
🐞 Bugfix
What changed? 🚀
Dependency objects that are shared between elements (brushes, transforms, image sources,
InputScopes, transitions, ...) kept the last element they were used by, or its DataContext, alive. Through that DataContext they could also keep the whole page alive after navigation. #25099 lists the reference chains.servicing/6.8already has two of the fixes: the weak single-parent association of a shareable value, with the guarded DataContext clear on release (#26142, the backport of #26123), and the ToolTip release (#26098, the backport of #25137 and #26096). This PR, rebased on currentservicing/6.8, ports what #25102 still adds on top of them. Five commits:fix(transform): Stop shared transforms rooting discarded elements: fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102's 86e5b66, with its follow-up 880cfd2 (popup test portable to WinUI) folded in. Same code as fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102.fix(transform): Register brush, geometry and group handlers weakly: fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102's c331145, plus the two native partials that only 6.x has (Transform.Android.csandTransform.Apple.cs), which now notify through the weak handlers instead of the removedChangedevent. Same code as fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102 otherwise.fix(dependencyobject): Hold a value's inherited DataContext weakly: the 6.x version of fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102's be0c5c1, explained below. Unchanged from the previous version of this PR.fix(dependencyobject): Drop a collected parent's DataContext: fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102's 0efaf28, adapted to how 6.8 pushes a DataContext, explained below.fix(dependencyobject): Read an attached owner's DataContext(b4bacab): 6.8 only. It fixes a throw in commit 4's DataContext comparison, and the same throw in the guarded clear on release thatservicing/6.8already has from fix: Stop shared values on plain DPs retaining their owners (backport #26123) #26142, explained below.Dropped from the previous version of this PR, because
servicing/6.8has them now or because #25102 dropped them: theToolTipServicecommit and its test, the weak_associatedParentRefand the ALC sweep and comment changes that went with it, theGiven_CollectibleAlcAssociationsrewrite, the rule that a collected parent counts as a second parent (andSingleParentNonFE_Parent_Collected_Then_Next_Parent_Counts_As_Second), and the unconditional DataContext clear inUnassociateParent(andSingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean). The guarded clear inUpdateChildBindablefrom #26142 is now the only clear on release.Commit 3: why 6.8 needs a different DataContext half
On master, #23547 removed
DataContextfrom every object that isn't a FrameworkElement. Such an object keeps the DataContext its element passes down in one plain field, and #25102 only had to make that field a weak reference.6.8 doesn't have #23547, and can't take it, because it removes public API. On 6.8 every dependency object still has a
DataContextproperty, and the value it inherits from its element is stored like any other property value, with a strong reference. So without commit 3, a shared value still keeps the view model of the last element that used it:InputScopeor the transitions in a sharedTransitionCollection, which keeps the last view model pushed into it, however many elements use it.It never grows (at most one view model per shared value), but the view model can reference its page.
Commit 3 makes only the inherited (Inheritance precedence)
DataContextof objects that aren't UIElements non-owning:DependencyObjectStoredecides once per object (originalObject is not UIElement) and tellsDependencyPropertyDetailsCollection, which applies it to theDataContextproperty only.DependencyPropertyDetailskeeps that value in one weak handle per object, of a private type so that no value set by user code can be taken for it. The handle is created with the first non-null value, and again after the value has been cleared; a change only retargets it, so it allocates nothing and takes no lock. Clearing the value frees the handle right away.GetEffectiveValue,GetBaseValueandGetInheritedValueunwrap it.VisualStatesetter that targets it, that object keeps its inherited DataContext strongly for the rest of its life, as all objects did before. DataContext is never coerced.What stays the same:
flyout.DataContext = viewModelin code, orDataContext="{Binding Sub}"on a brush.Visible differences, all the same as on master (where these objects have no
DataContextproperty) and in WinUI (which has no DataContext on them):DataContexton a brush, transform or other object that isn't a UIElement can return null after a garbage collection, once nothing else holds that view model (the element that provided it no longer has it), even if other elements still use the object. NoDataContextChangedevent is raised.No public API change.
Commit 4: a collected parent's DataContext
Since #26142, a shareable value whose single parent was collected without unassociating counts that parent as no parent, and the next owner becomes its single parent, as in WinUI, where a destroyed parent removes itself from its values (
CDependencyObject::ReleaseImpl->ResetReferencesFromChildren->RemoveParent). Two things were left behind:Background, for instance) the value's bindings kept resolving against the old view model whenever it was still alive. The weak storage of commit 3 doesn't help then._bindableHolderCountstayed one too high, and fix: Stop shared values on plain DPs retaining their owners (backport #26123) #26142's guarded clear on release (which waits for the last holder) never fired again for that value: the next owner's DataContext stayed behind once that owner let go.When
AssociateParentreuses an expired slot, it now clears the inherited DataContext unless it already is the new owner's, and leaves a value with its own parent other than the new owner alone (the same guard as the DataContext-change push,ApplyChildrenBindable).ReleaseCollectedParentfrees the slot and gives back the dead parent's holder count, there and in the ALC sweep's collected-parent branch.The difference from #25102: there the DataContext is cleared, and master's on-attach push in
UpdateChildBindablethen hands over the new owner's DataContext if it isn't null. 6.8 has no on-attach push. Its precedence-change push (TryApplyDataContextOnPrecedenceChange) runs beforeUpdateChildBindable, so a plain clear would drop the DataContext that push just gave the value; that is why the clear skips a DataContext that already is the new owner's. A reused value now ends where a value used for the first time ends in the same assignment: with the new owner's DataContext after a precedence change, and with none after a same-precedence replacement, where 6.8 pushes nothing into a new value either (unchanged) until the owner's DataContext changes.Commit 5: an owner whose values live in an attached store
Commit 4's comparison and #26142's guarded clear on release in
UpdateChildBindableboth read the owner's DataContext withGetValue(DependencyProperty). That overload checks the property's owner whenFeatureConfiguration.DependencyProperty.ValidatePropertyOwnerOnReadWriteis on, which is the default in Debug builds and in any build with a debugger attached. A plain object owner such asTextHighlighter, which isn't a dependency object on 6.8, keeps its values in an attached store. That store'sDataContextproperty is owned byAttachedDependencyObject, while the store's type isTextHighlighter, so the check threw "The Dependency Property [DataContext] is owned by [AttachedDependencyObject] and cannot be used on [TextHighlighter]".TextBlockhits commit 4's read on every draw with a selection: it creates aTextHighlighterfor its selection brush, and the previous highlighter has been collected by then. #26142's read throws when a highlighter's brush is replaced. These reads now go through the stores' DataContext details, asSetMentoredChildandRepropagateMentoredChildrenDataContextalready do. The values read don't change; only the owner check is skipped.#25102 and master's #26123 don't need this: on master only a FrameworkElement has a
DataContextproperty, and these reads go throughGetInheritedDataContextValue().Known remaining gap: shared flyouts
A flyout that is shared (app-level or static) and attached through
Button.Flyout,FlyoutBase.AttachedFlyout, or the flyout of aSplitButtonorDropDownButton, still keeps the view model of the last element it was attached to. With commit 3 the flyout's own DataContext is weak, but 6.8 copies it into UIElements the flyout owns, and those hold it strongly:Flyoutsets it on itsContentas a local value, whenever its DataContext changes, even if the flyout never opens;MenuFlyoutpasses it to its items, which are UIElements;FlyoutBasecopies it onto its popup as a local value.ContextFlyoutis less exposed: its DataContext is cleared when its element unloads.Master doesn't have this problem: it removed the popup copy, and #23547 changed how flyouts get their DataContext. Doing the same here would change 6.8's flyout DataContext behavior (content and items would no longer see the owner's DataContext while closed), which is out of scope for a servicing fix. This PR doesn't change it either way.
Performance
Commit 3 keeps one small object (32 bytes on 64-bit, holding a weak GC handle, with a finalizer) per object that receives a DataContext from its element. It is created with the first non-null value, and again after the value has been cleared (for example when the element of a
ContextFlyoutunloads, or when a shared value loses its only parent). Clearing frees it right away; otherwise its finalizer frees it when the object is collected. A DataContext change only retargets the handle: no allocation and no lock, so no extra garbage collections. Commit 4 only adds work when a value whose parent was collected is reused, and commit 5 makes the reads it touches skip the owner check; commits 1 and 2 don't touch the DataContext path.Time for 1000 DataContext changes on a panel of 20 controls, with the same probe as in the previous round: each run takes the median of 9 timed rounds after warm-up, and each build had 6 runs in rotating order. This round was measured on Skia desktop only, at this head, in one Release build where a temporary local switch turned commit 3's weak storage off for the "without" runs, so both columns run the same code otherwise. Other builds were running on the machine, so a run's median swung by up to 2x; the lowest timed round over the 6 runs is the steadier number.
Borderwith an inline brushButtonTextBoxThe medians are within the noise; the lowest rounds give +5% to +8%. The previous round, on the
servicing/6.8of that time and with little other load, measured +4% to +8% on Skia desktop (medians:Border2.5 ms to 2.6 ms,Button25.4 ms to 27.5 ms,TextBox95.7 ms to 101.1 ms) and +4% to +8% on WebAssembly (Chrome, interpreter; medians:Border55.5 ms to 59.8 ms,Button578 ms to 600 ms,TextBox1815 ms to 1950 ms; +2% to +8% across runs). I didn't measure WebAssembly again: commit 3 hasn't changed since, and the base changes since then (#26142 among them) are on code both builds share. What remains is the weak handle itself: reading or changing such a DataContext is a runtime call. Master pays the same kind of cost for its weak field. Page creation and teardown weren't measured separately; there, each affected object allocates its handle once and releases it once, as master does with its pooled handle.Tests
From #25102:
Given_Transform:When_RenderTransform_Shared_By_Style_Then_Discarded_Elements_Collected,When_RenderTransform_Shared_By_Popup_Style_Then_Discarded_Popups_Collected,When_Projection_Shared_By_Style_Then_Discarded_Elements_Collected,When_Transform_Shared_By_Brushes_Then_Discarded_Brushes_Collected,When_Transform_Shared_By_Geometries_Then_Discarded_Elements_Collected,When_Transform_Shared_By_TransformGroups_Then_Discarded_Groups_Collectedand the guardWhen_TransformGroup_Children_Cleared_Then_Removed_Child_Collected. Same file as in fix: Stop shared dependency objects retaining discarded elements and their DataContext #25102.Given_BindingMemoryLeak:When_BrushShared_SingleOwner_Removed_Then_Owner_And_ViewModel_Collected,When_TransitionsShared_Removed_Then_ViewModel_CollectedandWhen_InputScopeShared_Removed_Then_ViewModel_Collected. 6.x declaresTransitionsonFrameworkElement, and not at all on the native mobile targets, so the transitions test usesFrameworkElement.TransitionsPropertyand is compiled out there.Given_UIElement.DataContext:SingleParentNonFE_Parent_Collected_Then_Next_Parent_Starts_Clean(commit 4) and the guardSharedNonFE_ObjectBinding_Subtree_Severed_Then_Bound_Value_Released(commit 3).New for 6.x, in
Given_UIElement.DataContext:SingleParentNonFE_Parent_Collected_Then_Next_Parent_Gets_What_A_New_Value_Gets(commit 4), with a same-precedence replacement and with a precedence change: a bound brush whose first owner was collected while that owner's view model stays alive, assigned to a new owner that has a DataContext, never shows the dead owner's view model and ends with what a brush used for the first time gets in the same assignment (the new owner's DataContext after a precedence change, none after a same-precedence replacement).SingleParentNonFE_Parent_Collected_Then_Next_Parent_Has_Attached_StoreandSingleParentNonFE_Released_By_Owner_With_Attached_Store(commit 5): with the owner validation on, aTextHighlightertakes a brush whose first owner was collected (the brush must also drop the dead owner's DataContext), and replaces its brush, without throwing.NonFE_Inherited_DataContext_Survives_GC_While_Owner_Alive,NonFE_Binding_Added_After_DataContext_Pushed_Then_Resolves,NonFE_DataContext_Bound_To_Sub_Then_Resolves_And_UpdatesandNonFE_Local_DataContext_Stays_Owned(commit 3, unchanged): they force garbage collections and check that objects that aren't UIElements, and their bindings, still resolve to the right view model, never to the weak handle or to nothing, and that a DataContext set on the object itself stays owned.Validation
On Skia desktop (Release), at 97a2f17 (commit 4):
Given_Transform,Given_BindingMemoryLeak,Given_UIElement(including the DataContext tests),Given_CollectibleAlcAssociations,Given_ResourceDictionary_DataContext,Given_ToolTip,Given_FrameworkElement_And_Leak,Given_SharedTemplate_DataContext,Given_PlainDP_SharedValueandGiven_Flyout_PropertyPropagation: 700 test cases, 688 passed, 11 failed, 1 skipped. The 11 failures fail the same way on an untouchedservicing/6.8build on the same machine: 10Given_UIElementlayout-rounding tests at 125% scaling (ActualOffset,TransformToVisual, clip and nesting cases) andGiven_FrameworkElement_And_Leak.When_Add_Remove(CommandBarFlyout_Leak).At b4bacab (commit 5), on Skia desktop (Release), the same classes plus
Given_TextBlock: 851 test cases, 838 passed, 13 failed, 4 skipped. 11 failures are the ones above. The other two areGiven_TextBlock.When_Inlines_Transitively_Change(a pixel check) andWhen_IsTextSelectionEnabled_CRLF(a clipboard check, and other processes on the machine share the clipboard). They failed the same way on a 6.8 build without commits 4 and 5 on the same machine.Red and green, on Skia desktop, with a temporary local switch (not committed) that turns off parts of the product change:
SingleParentNonFE_Parent_Collected_Then_Next_Parent_Starts_Cleanand the same-precedence row ofSingleParentNonFE_Parent_Collected_Then_Next_Parent_Gets_What_A_New_Value_Getsfail: the brush keeps the dead owner's view model;Given_BindingMemoryLeakDataContext tests fail, the commit 4 tests pass.These two ran with an earlier form of commit 4 that set the new owner's DataContext instead of keeping it; its holder-count part and commit 3 are unchanged:
servicing/6.8DataContext code): the threeGiven_BindingMemoryLeakDataContext tests andStarts_Cleanfail; the otherGiven_BindingMemoryLeakandGiven_UIElementDataContext tests pass;Starts_Cleanfails at its last step.Commit 5 off: both of its tests fail with the owner validation exception, the first in
AssociateParentand the second inUpdateChildBindable's clear on release.With the full change, all of them pass. The transform tests are unchanged from the previous round, where they failed without their commits. Commit 3's
NonFE_*tests andSharedNonFE_ObjectBinding_Subtree_Severed_Then_Bound_Value_Releasedare guards: they pass with and without the change.In
Uno.UI.UnitTests(Release, net10.0), the only failures are 13 calendar tests (When_Gregorian_FixedDate*andWhen_DateTimeOffset_Is_Next_Day_If_Converted_To_Utc) that depend on the machine's time zone, as in the previous round.Not run this round: Skia WebAssembly runtime tests and the native
Uno.UIbuilds. Commits 1 to 3 are unchanged since the previous round, where they passed on WebAssembly and the native builds succeeded fornet10.0-android,net10.0-ios26.0,net10.0-maccatalyst26.0,net10.0-tvos26.0,net9.0-androidandnet9.0-ios18.0. Commits 4 and 5 only change shared C# inDependencyObjectStore.PR Checklist ✅
Screenshots Compare Test Runresults.