blob: e1e1b2dc7f5ca0b3df7bf4008539a85d5a3533a5 [file] [edit]
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; height: 40000px; }
/* A fixed, composited layer the scrolling thread must reposition on every
display refresh while scrolling. That reposition is what runs
CoordinatedSceneState::flushPendingState() on the scrolling thread. */
#fixed { position: fixed; top: 0; left: 0; width: 40px; height: 40px; background: green; will-change: transform; }
.box { position: absolute; left: 0; width: 30px; height: 30px; background: blue; will-change: transform; }
</style>
</head>
<body>
<div id="fixed"></div>
<div id="base"></div>
<div id="churn"></div>
<pre id="result">FAIL: did not finish</pre>
<script>
// This is a probabilistic regression test for a data race in
// CoordinatedSceneState: the main thread mutates m_layers in addLayer()/
// removeLayer() while the scrolling thread iterates it in flushPendingState().
//
// Reproducing it needs THREE things to overlap, all set up below:
// 1. hasRecentActivity() must stay true so the scrolling thread keeps
// ticking -- kept alive with a wheel event every frame (eventSender).
// (No 'wheel' listener / background-attachment:fixed here: those add
// synchronous scrolling reasons and disable scrolling-thread layer
// updates via canUpdateLayersOnScrollingThread().)
// 2. The main thread's rendering update must overrun the ~half-frame sync
// timeout so the scrolling thread DESYNCS and commits layer positions
// itself, concurrently with the main thread -- forced by churning a lot
// of composited layers per frame (slow compositing update).
// 3. addLayer()/removeLayer() must run during that slow update -- the churn
// itself provides them.
// Needs a Debug (asserts) build on a scrolling-thread port (GTK/WPE). On other
// ports it simply passes.
if (window.testRunner) {
testRunner.dumpAsText();
testRunner.waitUntilDone();
}
// Keep m_layers large so the scrolling thread's iteration stays open longer.
const base = document.getElementById('base');
for (let i = 0; i < 200; i++) {
const b = document.createElement('div');
b.className = 'box';
b.style.top = (i * 31) + 'px';
base.appendChild(b);
}
const churn = document.getElementById('churn');
const PER_FRAME = 150;
const DURATION_MS = 8000;
let previousGeneration = [];
function churnLayers()
{
for (const box of previousGeneration)
box.remove(); // detachLayer -> removeLayer
const generation = [];
for (let i = 0; i < PER_FRAME; i++) {
const b = document.createElement('div');
b.className = 'box';
b.style.top = (i * 31) + 'px';
churn.appendChild(b); // attachLayer -> addLayer
generation.push(b);
}
previousGeneration = generation;
}
const start = performance.now();
let direction = -1;
function tick()
{
// Wheel event every frame keeps hasProcessedWheelEventsRecently() true and
// scrolls the frame, so the scrolling thread repositions #fixed each refresh.
if (window.eventSender) {
eventSender.mouseScrollBy(0, direction * 40);
if (window.pageYOffset <= 0 || window.pageYOffset >= 39000)
direction = -direction;
}
churnLayers();
if (performance.now() - start < DURATION_MS) {
requestAnimationFrame(tick);
return;
}
for (const box of previousGeneration)
box.remove();
previousGeneration = [];
document.getElementById('result').textContent = "PASS: did not crash";
if (window.testRunner)
testRunner.notifyDone();
}
requestAnimationFrame(tick);
</script>
</body>
</html>