Changeset 293727 in webkit
- Timestamp:
- May 3, 2022 9:02:48 AM (3 months ago)
- Location:
- trunk
- Files:
-
- 7 edited
-
LayoutTests/ChangeLog (modified) (1 diff)
-
LayoutTests/inspector/view/asynchronous-layout-expected.txt (modified) (2 diffs)
-
LayoutTests/inspector/view/asynchronous-layout.html (modified) (2 diffs)
-
LayoutTests/inspector/view/basics-expected.txt (modified) (1 diff)
-
LayoutTests/inspector/view/basics.html (modified) (1 diff)
-
Source/WebInspectorUI/ChangeLog (modified) (1 diff)
-
Source/WebInspectorUI/UserInterface/Views/View.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/LayoutTests/ChangeLog
r293725 r293727 1 2022-05-03 Patrick Angle <pangle@apple.com> 2 3 Web Inspector: Importing a timeline leaves timeline overview non-scrollable/non-zoomable until windows is resized 4 https://bugs.webkit.org/show_bug.cgi?id=239880 5 6 Reviewed by Devin Rousso. 7 8 * inspector/view/asynchronous-layout-expected.txt: 9 * inspector/view/asynchronous-layout.html: 10 - Added test case for calls to `updateLayout()` during asynchronous `layout()`. 11 - Remove test case for removed `View.prototype.cancelLayout`. 12 13 * inspector/view/basics-expected.txt: 14 * inspector/view/basics.html: 15 - Added test case to verify that `_dirtyDescendantsCount` is an expected value at various points of 16 attaching/detaching views. 17 1 18 2022-05-03 Antti Koivisto <antti@apple.com> 2 19 -
trunk/LayoutTests/inspector/view/asynchronous-layout-expected.txt
r222782 r293727 21 21 PASS: View should not have a pending layout. 22 22 23 -- Running test case: View.SyncronousLayoutDuringAsyncronousLayout 24 PASS: Root view should have 2 dirty descendants. 25 PASS: Parent view should have 1 dirty descendant. 26 PASS: Child view should have 0 dirty descendants. 27 PASS: View should have a pending layout. 28 Child view completed a layout. 29 PASS: Root view should have 1 dirty descendant. 30 PASS: Parent view should have 0 dirty descendants. 31 PASS: Child view should have 0 dirty descendants. 32 PASS: Parent view should have started a layout. 33 PASS: Child view should have completed 1 layout. 34 Child view completed a layout. 35 PASS: Root view should have 0 dirty descendants. 36 PASS: Parent view should have 0 dirty descendants. 37 PASS: Child view should have 0 dirty descendants. 38 PASS: Parent view should have started a layout. 39 PASS: Child view should have completed 2 layouts. 40 Parent view completed a layout. 41 PASS: Root view should have 0 dirty descendants. 42 PASS: Root view should have 0 dirty descendants. 43 PASS: Root view should have 0 dirty descendants. 44 PASS: Parent view should have completed 1 layout. 45 PASS: Parent view should not have a pending layout. 46 23 47 -- Running test case: View.needsLayout.propogateToSubview 24 48 Schedule parent view update. … … 27 51 PASS: Child view should update its layout. 28 52 29 -- Running test case: View.cancelLayout30 Cancel automatic layout.31 PASS: View should not have a pending layout.32 Cancel scheduled layout.33 PASS: View should not have a pending layout.34 -
trunk/LayoutTests/inspector/view/asynchronous-layout.html
r222782 r293727 63 63 64 64 suite.addTestCase({ 65 name: "View.SyncronousLayoutDuringAsyncronousLayout", 66 test(resolve, reject) { 67 let UpdateLayoutTestView = class UpdateLayoutTestView extends WI.TestView { 68 layout() { 69 super.layout(); 70 71 this.subviews[0].updateLayout(); 72 } 73 } 74 75 let rootView = WI.View.rootView(); 76 let parentView = new UpdateLayoutTestView; 77 let childView = new WI.TestView; 78 79 rootView.addSubview(parentView); 80 parentView.addSubview(childView); 81 82 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 2, "Root view should have 2 dirty descendants."); 83 InspectorTest.expectEqual(parentView._dirtyDescendantsCount, 1, "Parent view should have 1 dirty descendant."); 84 InspectorTest.expectEqual(childView._dirtyDescendantsCount, 0, "Child view should have 0 dirty descendants."); 85 InspectorTest.expectThat(parentView.layoutPending, "View should have a pending layout."); 86 87 childView.evaluateAfterLayout(() => { 88 InspectorTest.log("Child view completed a layout."); 89 90 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 0, "Root view should have 1 dirty descendant."); 91 InspectorTest.expectEqual(parentView._dirtyDescendantsCount, 0, "Parent view should have 0 dirty descendants."); 92 InspectorTest.expectEqual(childView._dirtyDescendantsCount, 0, "Child view should have 0 dirty descendants."); 93 94 InspectorTest.expectEqual(parentView.layoutCount, 1, "Parent view should have started a layout."); 95 InspectorTest.expectEqual(childView.layoutCount, 1, "Child view should have completed 1 layout."); 96 97 childView.evaluateAfterLayout(() => { 98 InspectorTest.log("Child view completed a layout."); 99 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 0, "Root view should have 0 dirty descendants."); 100 InspectorTest.expectEqual(parentView._dirtyDescendantsCount, 0, "Parent view should have 0 dirty descendants."); 101 InspectorTest.expectEqual(childView._dirtyDescendantsCount, 0, "Child view should have 0 dirty descendants."); 102 103 InspectorTest.expectEqual(parentView.layoutCount, 1, "Parent view should have started a layout."); 104 InspectorTest.expectEqual(childView.layoutCount, 2, "Child view should have completed 2 layouts."); 105 }); 106 }); 107 108 parentView.evaluateAfterLayout(() => { 109 InspectorTest.log("Parent view completed a layout."); 110 111 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 0, "Root view should have 0 dirty descendants."); 112 InspectorTest.expectEqual(parentView._dirtyDescendantsCount, 0, "Root view should have 0 dirty descendants."); 113 InspectorTest.expectEqual(childView._dirtyDescendantsCount, 0, "Root view should have 0 dirty descendants."); 114 115 InspectorTest.expectEqual(parentView.layoutCount, 1, "Parent view should have completed 1 layout."); 116 InspectorTest.expectFalse(parentView.layoutPending, "Parent view should not have a pending layout."); 117 118 resolve(); 119 }); 120 } 121 }); 122 123 suite.addTestCase({ 65 124 name: "View.needsLayout.propogateToSubview", 66 125 test(resolve, reject) { … … 81 140 }); 82 141 83 suite.addTestCase({84 name: "View.cancelLayout",85 test(resolve, reject) {86 let view = new WI.TestView;87 WI.View.rootView().addSubview(view);88 89 InspectorTest.log("Cancel automatic layout.");90 view.cancelLayout();91 InspectorTest.expectFalse(view.layoutPending, "View should not have a pending layout.");92 93 InspectorTest.log("Cancel scheduled layout.");94 view.needsLayout();95 view.cancelLayout();96 InspectorTest.expectFalse(view.layoutPending, "View should not have a pending layout.");97 resolve();98 }99 });100 101 142 suite.runTestCasesAndFinish(); 102 143 } -
trunk/LayoutTests/inspector/view/basics-expected.txt
r242241 r293727 60 60 PASS: Should return null for null element. 61 61 62 -- Running test case: View.DirtyDescendantsCount 63 - Adding parent view to root view. 64 PASS: Root view should have 1 dirty descendant. 65 PASS: Root view should not be dirty. 66 PASS: Parent attached to root view should be dirty. 67 - Adding child view to parent view. 68 PASS: Root view should have 2 dirty descendants. 69 PASS: Parent attached to root view should have 1 dirty descendant. 70 PASS: Root view should not be dirty. 71 PASS: Parent attached to root view should be dirty. 72 PASS: Child attached to parent view should be dirty. 73 - Removing parent view from root view. 74 PASS: Root view should have 0 dirty descendants. 75 PASS: Parent detached from root view should have 0 dirty descendants. 76 PASS: Root view should not be dirty. 77 PASS: Parent detached from root view should not be dirty. 78 PASS: Child attached to detached parent view should not be dirty. 79 - Adding parent view to root view. 80 PASS: Root view should have 1 dirty descendant. 81 PASS: Parent detached from root view should have 0 dirty descendants. 82 PASS: Root view should not be dirty. 83 PASS: Parent attached to root view should be dirty. 84 PASS: Child attached to parent view should not be dirty. 85 -
trunk/LayoutTests/inspector/view/basics.html
r242241 r293727 180 180 }); 181 181 182 suite.addTestCase({ 183 name: "View.DirtyDescendantsCount", 184 test() { 185 let rootView = WI.View.rootView(); 186 let parent = new WI.View; 187 let child = new WI.View; 188 189 // The root view may still be dirty from a previous test removing a subview. 190 rootView._dirty = false; 191 rootView._dirtyDescendantsCount = 0; 192 193 InspectorTest.log("- Adding parent view to root view."); 194 rootView.addSubview(parent); 195 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 1, "Root view should have 1 dirty descendant."); 196 InspectorTest.expectFalse(rootView._dirty, "Root view should not be dirty."); 197 InspectorTest.expectTrue(parent._dirty, "Parent attached to root view should be dirty."); 198 199 InspectorTest.log("- Adding child view to parent view."); 200 parent.addSubview(child); 201 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 2, "Root view should have 2 dirty descendants."); 202 InspectorTest.expectEqual(parent._dirtyDescendantsCount, 1, "Parent attached to root view should have 1 dirty descendant."); 203 InspectorTest.expectFalse(rootView._dirty, "Root view should not be dirty."); 204 InspectorTest.expectTrue(parent._dirty, "Parent attached to root view should be dirty."); 205 InspectorTest.expectTrue(child._dirty, "Child attached to parent view should be dirty."); 206 207 InspectorTest.log("- Removing parent view from root view."); 208 rootView.removeSubview(parent); 209 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 0, "Root view should have 0 dirty descendants."); 210 InspectorTest.expectEqual(parent._dirtyDescendantsCount, 0, "Parent detached from root view should have 0 dirty descendants."); 211 InspectorTest.expectFalse(rootView._dirty, "Root view should not be dirty."); 212 InspectorTest.expectFalse(parent._dirty, "Parent detached from root view should not be dirty."); 213 InspectorTest.expectFalse(child._dirty, "Child attached to detached parent view should not be dirty."); 214 215 InspectorTest.log("- Adding parent view to root view."); 216 rootView.addSubview(parent); 217 InspectorTest.expectEqual(rootView._dirtyDescendantsCount, 1, "Root view should have 1 dirty descendant."); 218 InspectorTest.expectEqual(parent._dirtyDescendantsCount, 0, "Parent detached from root view should have 0 dirty descendants."); 219 InspectorTest.expectFalse(rootView._dirty, "Root view should not be dirty."); 220 InspectorTest.expectTrue(parent._dirty, "Parent attached to root view should be dirty."); 221 InspectorTest.expectFalse(child._dirty, "Child attached to parent view should not be dirty."); 222 223 // Clean up views for the next test. 224 rootView.removeSubview(parent); 225 226 return true; 227 } 228 }); 229 182 230 suite.runTestCasesAndFinish(); 183 231 } -
trunk/Source/WebInspectorUI/ChangeLog
r293724 r293727 1 2022-05-03 Patrick Angle <pangle@apple.com> 2 3 Web Inspector: Importing a timeline leaves timeline overview non-scrollable/non-zoomable until windows is resized 4 https://bugs.webkit.org/show_bug.cgi?id=239880 5 6 Reviewed by Devin Rousso. 7 8 Cancelling an in-progress layout (or the layout of a child of a view in middle of laying out) led to 9 `_dirtyDescendantsCount` being in an inconsistent state. Views had their `_dirtyDescendantsCount` cleared before 10 they are laid out, so every non-zero `_dirtyDescendantsCount` of a view in the process of being laid out meant 11 that during layout `needsLayout()` was called to dirty that view or one of its children. In this case, a 12 previous cancellation meant that the TimelineOverview ended up in an inconsistent state where its parent view 13 tree was not aware that it needed layout because it reached a zero `_dirtyDescendantsCount` while walking down 14 the view hierarchy, which meant that the TimelineOverview was perpetually stuck until the entire view hierarchy 15 needed re-laid out due to a window resize or similar event. 16 17 Our current accounting for `_dirtyDescendantsCount` in View makes assumptions that are not always true during 18 layout. Specifically that parents of views undergoing layout will have a non-zero `_dirtyDescendantsCount` still 19 from which to subtract in `_cancelScheduledLayoutForView`. This wasn't always true because as soon as we began 20 laying out a parent view, we cleared both the `_dirty` flag as well as zeroed out `_dirtyDescendantsCount`. In 21 the case of cancelling a layout from within a layout, the cancellation wouldn't actually take effect anyways, 22 and since `cancelLayout` was only ever invoked from `updateLayout`, the relevant layout flags should instead be 23 cleared as part of actually performing the layout. `cancelLayout` at most saved us a rAF callback only to 24 realize we no longer have any views in need of layout, but this benefit was negligible, and in general we should 25 prefer not to be calling `updateLayout` for performance reasons anyways. 26 27 This patch improves the accounting around `_dirtyDescendantsCount`, making sure to decrement it as we perform 28 layout, instead of all at once. This reduces our reliance on assumptions about who will modify the 29 `_dirtyDescendantsCount` and at what times relative to layout. 30 31 The only time in this patch that we do not do a +1/-1 adjustment to the layout count is in the special case of 32 attaching/detaching a view. In that case, the parent tree from which we are detaching the view will have the 33 `_dirtyDescendantsCount` of the detached parent adjusted by the `_dirtyDescendantsCount` of the former child 34 view. This is done by `_setSelfAndDescendantsNotDirty`, which allows us to avoid an exponential walking of the 35 tree for this otherwise self-contained operation (we don't call into any overridable functions that could modify 36 the `_dirty` flag or `_dirtyDescendantsCount`). 37 38 After we have cleaned up the former parent branch of the tree, we mark the newly attached view as needing 39 layout, which will then ensure the child view gets laid out again by marking it as `dirty` and incrementing its 40 new parent branch of the tree's `_dirtyDescendantsCount`. 41 42 * UserInterface/Views/View.js: 43 (WI.View.prototype.insertSubviewBefore): 44 - Add assertion that the view is not currently a child of a different view in addition to the existing check 45 that a view is not already a child of the target view. 46 47 (WI.View.prototype._setDirty): 48 - New utility method through which (almost) all marking of dirty/not dirty should be done in order to ensure 49 that `_dirtyDescendantsCount` is correctly adjusted. 50 51 (WI.View.prototype._didMoveToParent): 52 - Mark the view and all subviews as not dirty before removing it from its previous parent in order to adjust 53 `_dirtyDescendantsCount` appropriately, and then mark the view as dirty once attached to its new parent. 54 - Inline the logic from WI.View.prototype._didMoveToWindow in order to also handle marking children as not dirty 55 and having a zeroed `_dirtyDescendantsCount`. 56 - This method contains the exception to the rule that _dirtyDescendantsCount is always incremented/decremented 57 by one. Here we are able to optimize away the need to walk the entire parent tree for each subview/subviews of 58 subviews/etc. because the operation takes place all at once with no overridable function being called where we 59 have to worry about mutations to `_dirty`/`_dirtyDescendantsCount`. 60 61 (WI.View.prototype._layoutSubtree): 62 (WI.View._visitViewTreeForLayout): 63 - Don't zero out the `_dirtyDescendantsCount`, and instead use the new `_setDirty` helper. 64 65 (WI.View._scheduleLayoutForView): 66 - Don't zero out the `_dirtyDescendantsCount`, and instead use the new `_setDirty` helper. 67 - Mark the view as dirty after checking if its attached a view so that detached views have no dirty state until 68 they are attached, at which point the root of the previous detached subtree will be marked as dirty. 69 - Drive-by change to use a for-loop instead of a while-loop to avoid `Array.prototype.shift()`. 70 71 (WI.View.prototype.updateLayout): 72 (WI.View.prototype.cancelLayout): Deleted. 73 (WI.View._cancelScheduledLayoutForView): Deleted. 74 - Remove the concept of "canceling" layout, since it is only used by `updateLayout`, and the call to 75 `_layoutSubtree` in `updateLayout` will cause the view, dirty or not, to be marked as not dirty. 76 1 77 2022-05-03 Michael Saboff <msaboff@apple.com> 2 78 -
trunk/Source/WebInspectorUI/UserInterface/Views/View.js
r276975 r293727 96 96 console.assert(view !== WI.View._rootView, "Root view cannot be a subview."); 97 97 98 console.assert(!view.parentView, view); 99 98 100 if (this._subviews.includes(view)) { 99 101 console.assert(false, "Cannot add view that is already a subview.", view); … … 154 156 updateLayout(layoutReason) 155 157 { 156 this.cancelLayout();157 158 158 this._setLayoutReason(layoutReason); 159 159 this._layoutSubtree(); … … 176 176 177 177 WI.View._scheduleLayoutForView(this); 178 }179 180 cancelLayout()181 {182 WI.View._cancelScheduledLayoutForView(this);183 178 } 184 179 … … 231 226 // Private 232 227 228 _setDirty(dirty) 229 { 230 if (this._dirty === dirty) 231 return; 232 233 this._dirty = dirty; 234 235 for (let parentView = this.parentView; parentView; parentView = parentView.parentView) { 236 parentView._dirtyDescendantsCount += this._dirty ? 1 : -1; 237 console.assert(parentView._dirtyDescendantsCount >= 0); 238 } 239 } 240 233 241 _didMoveToParent(parentView) 234 242 { 243 if (this._parentView === parentView) 244 return; 245 246 console.assert(this._parentView || !(this._isDirty || this._dirtyDescendantsCount)); 247 248 let dirtyDescendantsCount = this._dirtyDescendantsCount; 249 if (this._dirty) 250 ++dirtyDescendantsCount; 251 252 if (dirtyDescendantsCount) { 253 for (let view = this.parentView; view; view = view.parentView) { 254 view._dirtyDescendantsCount -= dirtyDescendantsCount; 255 console.assert(view._dirtyDescendantsCount >= 0); 256 } 257 } 258 235 259 this._parentView = parentView; 236 237 260 let isAttachedToRoot = this.isDescendantOf(WI.View._rootView); 238 this._didMoveToWindow(isAttachedToRoot); 239 240 if (!this._parentView) 241 return; 242 243 let pendingLayoutsCount = this._dirtyDescendantsCount; 244 if (this._dirty) 245 pendingLayoutsCount++; 246 247 let view = this._parentView; 248 while (view) { 249 view._dirtyDescendantsCount += pendingLayoutsCount; 250 view = view.parentView; 251 } 252 } 253 254 _didMoveToWindow(isAttachedToRoot) 255 { 256 if (this._isAttachedToRoot === isAttachedToRoot) 257 return; 258 259 this._isAttachedToRoot = isAttachedToRoot; 260 if (this._isAttachedToRoot) { 261 262 let views = [this]; 263 for (let i = 0; i < views.length; ++i) { 264 let view = views[i]; 265 views.pushAll(view.subviews); 266 267 view._dirty = false; 268 view._dirtyDescendantsCount = 0; 269 270 if (view._isAttachedToRoot === isAttachedToRoot) 271 continue; 272 273 view._isAttachedToRoot = isAttachedToRoot; 274 if (view._isAttachedToRoot) 275 view.attached(); 276 else 277 view.detached(); 278 } 279 280 if (isAttachedToRoot) 261 281 WI.View._scheduleLayoutForView(this); 262 this.attached();263 } else {264 if (this._dirty)265 this.cancelLayout();266 this.detached();267 }268 269 for (let view of this._subviews)270 view._didMoveToWindow(isAttachedToRoot);271 282 } 272 283 273 284 _layoutSubtree() 274 285 { 275 this._dirty = false; 276 this._dirtyDescendantsCount = 0; 286 this._setDirty(false); 277 287 let isInitialLayout = !this._didInitialLayout; 278 288 … … 345 355 static _scheduleLayoutForView(view) 346 356 { 347 view._dirty = true;348 349 let parentView = view.parentView;350 while (parentView) {351 parentView._dirtyDescendantsCount++;352 parentView = parentView.parentView;353 }354 355 357 if (!view._isAttachedToRoot) 356 358 return; 357 359 360 view._setDirty(true); 361 358 362 if (WI.View._scheduledLayoutUpdateIdentifier) 359 363 return; … … 362 366 } 363 367 364 static _cancelScheduledLayoutForView(view) 365 { 366 let cancelledLayoutsCount = view._dirtyDescendantsCount; 367 if (view.layoutPending) 368 cancelledLayoutsCount++; 369 370 let parentView = view.parentView; 371 while (parentView) { 372 parentView._dirtyDescendantsCount = Math.max(0, parentView._dirtyDescendantsCount - cancelledLayoutsCount); 373 parentView = parentView.parentView; 374 } 375 376 view._dirty = false; 377 378 if (!WI.View._scheduledLayoutUpdateIdentifier) 379 return; 380 381 let rootView = WI.View._rootView; 382 if (!rootView || rootView._dirtyDescendantsCount) 383 return; 384 385 // No views need layout, so cancel the pending requestAnimationFrame. 386 cancelAnimationFrame(WI.View._scheduledLayoutUpdateIdentifier); 368 static _visitViewTreeForLayout() 369 { 370 console.assert(WI.View._rootView, "Cannot layout view tree without a root."); 371 387 372 WI.View._scheduledLayoutUpdateIdentifier = undefined; 388 }389 390 static _visitViewTreeForLayout()391 {392 console.assert(WI.View._rootView, "Cannot layout view tree without a root.");393 394 WI.View._scheduledLayoutUpdateIdentifier = undefined;395 373 396 374 let views = [WI.View._rootView]; 397 while (views.length) {398 let view = views .shift();375 for (let i = 0; i < views.length; ++i) { 376 let view = views[i]; 399 377 if (view.layoutPending) 400 378 view._layoutSubtree(); 401 else if (view._dirtyDescendantsCount) {379 else if (view._dirtyDescendantsCount) 402 380 views.pushAll(view.subviews); 403 view._dirtyDescendantsCount = 0;404 }405 381 } 406 382 }
Note: See TracChangeset
for help on using the changeset viewer.