tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>