test_layerization.html (13562B)
1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 https://bugzilla.mozilla.org/show_bug.cgi?id=1173580 5 --> 6 <head> 7 <title>Test for layerization</title> 8 <script src="/tests/SimpleTest/SimpleTest.js"></script> 9 <script src="/tests/SimpleTest/EventUtils.js"></script> 10 <script src="/tests/SimpleTest/paint_listener.js"></script> 11 <script type="application/javascript" src="apz_test_native_event_utils.js"></script> 12 <script type="application/javascript" src="apz_test_utils.js"></script> 13 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> 14 <link rel="stylesheet" type="text/css" href="helper_subframe_style.css"/> 15 <style> 16 #container { 17 display: flex; 18 overflow: scroll; 19 height: 500px; 20 } 21 .outer-frame { 22 height: 500px; 23 overflow: scroll; 24 flex-basis: 100%; 25 background: repeating-linear-gradient(#CCC, #CCC 100px, #BBB 100px, #BBB 200px); 26 } 27 #container-content { 28 height: 200%; 29 } 30 </style> 31 </head> 32 <body> 33 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173580">APZ layerization tests</a> 34 <p id="display"></p> 35 <div id="container"> 36 <div id="outer1" class="outer-frame"> 37 <div id="inner1" class="inner-frame"> 38 <div class="inner-content"></div> 39 </div> 40 </div> 41 <div id="outer2" class="outer-frame"> 42 <div id="inner2" class="inner-frame"> 43 <div class="inner-content"></div> 44 </div> 45 </div> 46 <iframe id="outer3" class="outer-frame" src="helper_iframe1.html"></iframe> 47 <iframe id="outer4" class="outer-frame" src="helper_iframe2.html"></iframe> 48 <!-- The container-content div ensures 'container' is scrollable, so the 49 optimization that layerizes the primary async-scrollable frame on page 50 load layerizes it rather than its child subframes. --> 51 <div id="container-content"></div> 52 </div> 53 <pre id="test"> 54 <script type="application/javascript"> 55 56 // Scroll the mouse wheel over |element|. 57 async function scrollWheelOver(element) { 58 await promiseMoveMouseAndScrollWheelOver(element, 10, 10, /* waitForScroll = */ false); 59 } 60 61 const DISPLAYPORT_EXPIRY = 100; 62 63 let config = getHitTestConfig(); 64 let activateAllScrollFrames = config.activateAllScrollFrames; 65 66 let heightMultiplier = SpecialPowers.getCharPref("apz.y_stationary_size_multiplier"); 67 // The effective height multiplier can be reduced for alignment reasons. 68 // The reduction should be no more than a factor of two. 69 heightMultiplier /= 2; 70 info("effective displayport height multipler is " + heightMultiplier); 71 72 function hasNonZeroMarginDisplayPort(elementId, containingDoc = null) { 73 let dp = getLastContentDisplayportFor(elementId); 74 if (dp == null) { 75 return false; 76 } 77 let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId); 78 info(elementId); 79 info("window size " + window.innerWidth + " " + window.innerHeight); 80 info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height); 81 info("eltsize " + elt.clientWidth + " " + elt.clientHeight); 82 return dp.height >= heightMultiplier * Math.min(elt.clientHeight, window.innerHeight); 83 } 84 85 function hasMinimalDisplayPort(elementId, containingDoc = null) { 86 let dp = getLastContentDisplayportFor(elementId); 87 if (dp == null) { 88 return false; 89 } 90 let elt = (containingDoc != null ? containingDoc : document).getElementById(elementId); 91 info(elementId); 92 info("dp " + dp.x + " " + dp.y + " " + dp.width + " " + dp.height); 93 info("eltsize " + elt.clientWidth + " " + elt.clientHeight); 94 return dp.width <= (elt.clientWidth + 2) && dp.height <= (elt.clientHeight + 2); 95 } 96 97 function checkDirectActivation(elementId, containingDoc = null) { 98 if (activateAllScrollFrames) { 99 return hasNonZeroMarginDisplayPort(elementId, containingDoc); 100 } 101 return isLayerized(elementId); 102 103 } 104 105 function checkAncestorActivation(elementId, containingDoc = null) { 106 if (activateAllScrollFrames) { 107 return hasMinimalDisplayPort(elementId, containingDoc); 108 } 109 return isLayerized(elementId); 110 111 } 112 113 function checkInactive(elementId, containingDoc = null) { 114 if (activateAllScrollFrames) { 115 return hasMinimalDisplayPort(elementId, containingDoc); 116 } 117 return !isLayerized(elementId); 118 119 } 120 121 async function test() { 122 await SpecialPowers.pushPrefEnv({ 123 "set": [ 124 // Causes the test to intermittently fail on ASAN opt linux. 125 ["mousewheel.system_scroll_override.enabled", false], 126 ] 127 }); 128 129 let outer3Doc = document.getElementById("outer3").contentDocument; 130 let outer4Doc = document.getElementById("outer4").contentDocument; 131 132 // Initially, everything should be inactive. 133 ok(checkInactive("outer1"), "initially 'outer1' should not be active"); 134 ok(checkInactive("inner1"), "initially 'inner1' should not be active"); 135 ok(checkInactive("outer2"), "initially 'outer2' should not be active"); 136 ok(checkInactive("inner2"), "initially 'inner2' should not be active"); 137 ok(checkInactive("outer3"), "initially 'outer3' should not be active"); 138 ok(checkInactive("inner3", outer3Doc), 139 "initially 'inner3' should not be active"); 140 ok(checkInactive("outer4"), "initially 'outer4' should not be active"); 141 ok(checkInactive("inner4", outer4Doc), 142 "initially 'inner4' should not be active"); 143 144 // Scrolling over outer1 should activate outer1 directly, but not inner1. 145 await scrollWheelOver(document.getElementById("outer1")); 146 await promiseAllPaintsDone(); 147 await promiseOnlyApzControllerFlushed(); 148 ok(checkDirectActivation("outer1"), 149 "scrolling 'outer1' should activate it directly"); 150 ok(checkInactive("inner1"), 151 "scrolling 'outer1' should not cause 'inner1' to get activated"); 152 153 // Scrolling over inner2 should activate inner2 directly, but outer2 only ancestrally. 154 await scrollWheelOver(document.getElementById("inner2")); 155 await promiseAllPaintsDone(); 156 await promiseOnlyApzControllerFlushed(); 157 ok(checkDirectActivation("inner2"), 158 "scrolling 'inner2' should cause it to be directly activated"); 159 ok(checkAncestorActivation("outer2"), 160 "scrolling 'inner2' should cause 'outer2' to be activated as an ancestor"); 161 162 // The second half of the test repeats the same checks as the first half, 163 // but with an iframe as the outer scrollable frame. 164 165 // Scrolling over outer3 should activate outer3 directly, but not inner3. 166 await scrollWheelOver(outer3Doc.documentElement); 167 await promiseAllPaintsDone(); 168 await promiseOnlyApzControllerFlushed(); 169 ok(checkDirectActivation("outer3"), "scrolling 'outer3' should cause it to be directly activated"); 170 ok(checkInactive("inner3", outer3Doc), 171 "scrolling 'outer3' should not cause 'inner3' to be activated"); 172 173 // Scrolling over inner4 should activate inner4 directly, but outer4 only ancestrally. 174 await scrollWheelOver(outer4Doc.getElementById("inner4")); 175 await promiseAllPaintsDone(); 176 await promiseOnlyApzControllerFlushed(); 177 ok(checkDirectActivation("inner4", outer4Doc), 178 "scrolling 'inner4' should cause it to be directly activated"); 179 ok(checkAncestorActivation("outer4"), 180 "scrolling 'inner4' should cause 'outer4' to be activated"); 181 182 // Now we enable displayport expiry, and verify that things are still 183 // activated as they were before. 184 await SpecialPowers.pushPrefEnv({"set": [["apz.displayport_expiry_ms", DISPLAYPORT_EXPIRY]]}); 185 ok(checkDirectActivation("outer1"), "outer1 still has non zero display port after enabling expiry"); 186 ok(checkInactive("inner1"), "inner1 is still has zero margin display port after enabling expiry"); 187 ok(checkAncestorActivation("outer2"), "outer2 still has zero margin display port after enabling expiry"); 188 ok(checkDirectActivation("inner2"), "inner2 still has non zero display port after enabling expiry"); 189 ok(checkDirectActivation("outer3"), "outer3 still has non zero display port after enabling expiry"); 190 ok(checkInactive("inner3", outer3Doc), 191 "inner3 still has zero margin display port after enabling expiry"); 192 ok(checkDirectActivation("inner4", outer4Doc), 193 "inner4 still has non zero display port after enabling expiry"); 194 ok(checkAncestorActivation("outer4"), "outer4 still has zero margin display port after enabling expiry"); 195 196 // Now we trigger a scroll on some of the things still layerized, so that 197 // the displayport expiry gets triggered. 198 199 // Expire displayport with scrolling on outer1 200 await scrollWheelOver(document.getElementById("outer1")); 201 await promiseAllPaintsDone(); 202 await promiseOnlyApzControllerFlushed(); 203 await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY); 204 await promiseAllPaintsDone(); 205 ok(checkInactive("outer1"), "outer1 is inactive after displayport expiry"); 206 ok(checkInactive("inner1"), "inner1 is inactive after displayport expiry"); 207 208 // Expire displayport with scrolling on inner2 209 await scrollWheelOver(document.getElementById("inner2")); 210 await promiseAllPaintsDone(); 211 await promiseOnlyApzControllerFlushed(); 212 // Once the expiry elapses, it will trigger expiry on outer2, so we check 213 // both, one at a time. 214 await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY); 215 await promiseAllPaintsDone(); 216 ok(checkInactive("inner2"), "inner2 is inactive after displayport expiry"); 217 await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY); 218 await promiseAllPaintsDone(); 219 ok(checkInactive("outer2"), "outer2 is inactive with inner2"); 220 221 // We need to wrap the next bit in a loop and keep retrying until it 222 // succeeds. Let me explain why this is the best option at this time. Below 223 // we scroll over inner3, this triggers a 100 ms timer to expire it's display 224 // port. Then when it expires it schedules a paint and triggers another 225 // 100 ms timer on it's parent, outer3, to expire. The paint needs to happen 226 // before the timer fires because the paint is what updates 227 // mIsParentToActiveScrollFrames on outer3, and mIsParentToActiveScrollFrames 228 // being true blocks a display port from expiring. It was true because it 229 // contained inner3, but no longer. In real life the timer is 15000 ms so a 230 // paint will happen, but here in a test the timer is 100 ms so that paint 231 // can not happen in time. We could add some more complication to this code 232 // just for this test, or we could just loop here. 233 let itWorked = false; 234 while (!itWorked) { 235 // Scroll on inner3. inner3 isn't layerized, and this will cause it to 236 // get layerized, but it will also trigger displayport expiration for inner3 237 // which will eventually trigger displayport expiration on inner3 and outer3. 238 // Note that the displayport expiration might actually happen before the wheel 239 // input is processed in the compositor (see bug 1246480 comment 3), and so 240 // we make sure not to wait for a scroll event here, since it may never fire. 241 // However, if we do get a scroll event while waiting for the expiry, we need 242 // to restart the expiry timer because the displayport expiry got reset. There's 243 // no good way that I can think of to deterministically avoid doing this. 244 let inner3 = outer3Doc.getElementById("inner3"); 245 await scrollWheelOver(inner3); 246 await promiseAllPaintsDone(); 247 await promiseOnlyApzControllerFlushed(); 248 let timerPromise = new Promise(resolve => { 249 var timeoutTarget = function() { 250 inner3.removeEventListener("scroll", timeoutResetter); 251 resolve(); 252 }; 253 var timerId = setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY); 254 var timeoutResetter = function() { 255 ok(true, "Got a scroll event; resetting timer..."); 256 clearTimeout(timerId); 257 setTimeout(timeoutTarget, DISPLAYPORT_EXPIRY); 258 // by not updating timerId we ensure that this listener resets the timeout 259 // at most once. 260 }; 261 inner3.addEventListener("scroll", timeoutResetter); 262 }); 263 await timerPromise; // wait for the setTimeout to elapse 264 265 await promiseAllPaintsDone(); 266 ok(checkInactive("inner3", outer3Doc), 267 "inner3 is inactive after expiry"); 268 await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY); 269 await promiseAllPaintsDone(); 270 if (checkInactive("outer3")) { 271 ok(true, "outer3 is inactive after inner3 triggered expiry"); 272 itWorked = true; 273 } 274 } 275 276 // Scroll outer4 and wait for the expiry. It should NOT get expired because 277 // inner4 is still layerized 278 await scrollWheelOver(outer4Doc.documentElement); 279 await promiseAllPaintsDone(); 280 await promiseOnlyApzControllerFlushed(); 281 // Wait for the expiry to elapse 282 await SpecialPowers.promiseTimeout(DISPLAYPORT_EXPIRY); 283 await promiseAllPaintsDone(); 284 ok(checkDirectActivation("inner4", outer4Doc), 285 "inner4 still is directly activated because it never expired"); 286 ok(checkDirectActivation("outer4"), 287 "outer4 still still is directly activated because inner4 is still layerized"); 288 } 289 290 if (isApzEnabled()) { 291 SimpleTest.waitForExplicitFinish(); 292 SimpleTest.requestFlakyTimeout("we are testing code that measures an actual timeout"); 293 SimpleTest.expectAssertions(0, 8); // we get a bunch of "ASSERTION: Bounds computation mismatch" sometimes (bug 1232856) 294 295 // Disable smooth scrolling, because it results in long-running scroll 296 // animations that can result in a 'scroll' event triggered by an earlier 297 // wheel event as corresponding to a later wheel event. 298 // Also enable APZ test logging, since we use that data to determine whether 299 // a scroll frame was layerized. 300 pushPrefs([["general.smoothScroll", false], 301 ["apz.displayport_expiry_ms", 0], 302 ["apz.test.logging_enabled", true]]) 303 .then(waitUntilApzStable) 304 .then(test) 305 .then(SimpleTest.finish, SimpleTest.finishWithFailure); 306 } 307 308 </script> 309 </pre> 310 </body> 311 </html>