Take an outer scroll container A that is async-inactive because it has zero scroll range. It contains various active scroll frames and active nested scroll frames. Some content inside A (but not related to any of the active descendant scroll frames) is modified so that the size of the scrolled content increases making the scroll range non-zero and now WantAsyncScroll() is true. The size increase triggers a retained display list update. None of the descendant already-active scroll frames are marked modified, so the display list build does not visit the every active descendant scroll frame (even if we were to visit every direct ASR child of A). Visiting the frame of an ASR is the only way that we can update it to changes in the ASR tree, so some ASRs do not get updated, so the depth values get out of sync and we hit the depth invariant assert. The most obvious way to fix this is to check after reflow if a scroll frame has changed scroll range that changed the return value of WantAsyncScroll and mark it modified. But this is too large of a hammer because the extra display list building the modified frames will trigger, but also because WantAsyncScroll is relatively heavy. As such we limit the check as narrowly as we can. We use a bool that tracks scroll frames that were inactive during the previous paint but that have active scroll frame descendants. Further, previous work in bug 2044231 reduced the cost of WantAsyncScroll by caching GetScrolledRect during ReflowFinished. The main cost of WantAsyncScroll will then be the memory load of fetching the overflow values from the style display struct which might not be cached at this point (even though they are fetched at the start of the reflow of this scroll frame) because this happens after the reflow of the entire frame tree. Lower down in ReflowFinished we call GetScrollRangeForUserInputEvents() inside |if (mVScrollbarBox || mHScrollbarBox)| which will also fetch those values. So hopefully this should not add many memory fetches. I also checked that anything else that can change WantAsyncScroll's return value should not have this problem. Differential Revision: https://phabricator.services.mozilla.com/D304399
56 lines
1.7 KiB
HTML
56 lines
1.7 KiB
HTML
<!DOCTYPE html>
|
|
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2042439">
|
|
<!-- Scroll-only variant (no position:sticky). A nested async-active scroll
|
|
container (#C) sits behind a will-change:transform stacking-context
|
|
boundary, inside #B, inside an outer element (#A) that starts
|
|
async-inactive because it has no scrollable overflow. Growing #spacer
|
|
makes #A a scroll container and async-active during a partial display list
|
|
update; #C is not revisited (retained behind the boundary), so its cached
|
|
scroll-tree node depth must not go stale. -->
|
|
<html class="test-wait">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body { margin: 0; }
|
|
* { scrollbar-width: none; }
|
|
|
|
#outer { height: 300px; overflow-y: auto; }
|
|
#outer-content { height: 3000px; }
|
|
|
|
#A { height: 460px; overflow-y: auto; width: 400px; }
|
|
#B { height: 200px; overflow-y: auto; background: #eef; }
|
|
.wct { will-change: transform; }
|
|
#C { height: 100px; overflow-y: auto; background: #efe; }
|
|
#spacer { height: 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="outer">
|
|
<div id="outer-content">
|
|
<div id="A">
|
|
<div id="B">
|
|
<div class="wct">
|
|
<div id="C">
|
|
<div style="height:200px;background:blue"></div>
|
|
</div>
|
|
</div>
|
|
<div style="height:200px;background:green"></div>
|
|
</div>
|
|
<div id="spacer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function raf() { return new Promise(r => requestAnimationFrame(r)); }
|
|
onload = async () => {
|
|
await raf();
|
|
await raf();
|
|
document.getElementById("spacer").style.height = "400px";
|
|
await raf();
|
|
await raf();
|
|
document.documentElement.classList.remove("test-wait");
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|