fixtures/scheduler/index.html (695 lines of code) (raw):

<!DOCTYPE html> <html style="width: 100%; height: 100%;"> <head> <meta charset="utf-8"> <title>Scheduler Test Page</title> <style> .correct { border: solid green 2px; } .incorrect { border: dashed red 2px; } </style> </head> <body> <h1>Scheduler Fixture</h1> <p> This fixture is for manual testing purposes, and the patterns used in implementing it should not be used as a model. This is mainly for anyone working on making changes to the `schedule` module. </p> <h2>Tests:</h2> <ol> <li> <button onClick="runTestOne()">Run Test 1</button> <p>Calls the callback within the frame when not blocked:</p> <div><b>Expected:</b></div> <div id="test-1-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-1"></div> </li> <li> <p>Accepts multiple callbacks and calls within frame when not blocked</p> <button onClick="runTestTwo()">Run Test 2</button> <div><b>Expected:</b></div> <div id="test-2-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-2"></div> </li> <li> <p>Schedules callbacks in correct order when they use scheduleCallback to schedule themselves</p> <button onClick="runTestThree()">Run Test 3</button> <div><b>Expected:</b></div> <div id="test-3-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-3"></div> </li> <li> <p>Calls timed out callbacks and then any more pending callbacks, defers others if time runs out</p> <button onClick="runTestFour()">Run Test 4</button> <div><b>Expected:</b></div> <div id="test-4-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-4"></div> </li> <li> <p>When some callbacks throw errors, still calls them all within the same frame</p> <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p> <button onClick="runTestFive()">Run Test 5</button> </li> <li> <p>When some callbacks throw errors <b> and some also time out</b>, still calls them all within the same frame</p> <p><b>IMPORTANT:</b> Open the console when you run this! Inspect the logs there!</p> <button onClick="runTestSix()">Run Test 6</button> </li> <li> <p>Continues calling callbacks even when user switches away from this tab</p> <button onClick="runTestSeven()">Run Test 7</button> <div><b>Click the button above, observe the counter, then switch to another tab and switch back:</b></div> <div id="test-7"> </div> <div> If the counter advanced while you were away from this tab, it's correct.</div> </li> <li> <p>Can pause execution, dump scheduled callbacks, and continue where it left off</p> <button onClick="runTestEight()">Run Test 8</button> <div><b>Click the button above, press "continue" to finish the test after it pauses:</b></div> <button onClick="continueTestEight()">continue</button> <div><b>Expected:</b></div> <div id="test-8-expected"> </div> <div> -------------------------------------------------</div> <div> If the test didn't progress until you hit "continue" and </div> <div> you see the same above and below afterwards it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-8"></div> </li> <li> <p>Can force a specific framerate</p> <p><b>IMPORTANT:</b> This test may be flaky if other tests have been run in this js instance. To get a clean test refresh the page before running test 9</p> <button onClick="runTestNine()">Run Test 9</button> <div><b>Expected:</b></div> <div id="test-9-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-9"></div> </div> </li> <li> <p>Runs scheduled JS work for 99% of the frame time when nothing else is using the thread.</p> <p><b>NOTE:</b> Try this test both when nothing else is running and when something is using the compositor thread in another visible tab with video or <a href="https://www.shadertoy.com/view/MtffDX">WebGL content</a> (Shift+Click).</p> <button onClick="runTestTen()">Run Test 10</button> <div><b>Expected:</b></div> <div id="test-10-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-10"></div> </div> </li> <li> <p>Runs scheduled JS work more than 95% of the frame time when inserting DOM nodes.</p> <p><b>NOTE:</b> Try this test both when nothing else is running and when something is using the compositor thread in another visible tab with video or <a href="https://www.shadertoy.com/view/MtffDX">WebGL content</a> (Shift+Click).</p> <button onClick="runTestEleven()">Run Test 11</button> <div><b>Expected:</b></div> <div id="test-11-expected"> </div> <div> -------------------------------------------------</div> <div> If you see the same above and below it's correct. <div> -------------------------------------------------</div> <div><b>Actual:</b></div> <div id="test-11"></div> </div> </li> </ol> <script src="../../build/node_modules/react/umd/react.production.min.js"></script> <script src="../../build/node_modules/scheduler/umd/scheduler.production.min.js"></script> <script src="https://unpkg.com/babel-standalone@6/babel.js"></script> <script type="text/babel"> const { unstable_scheduleCallback: scheduleCallback, unstable_cancelCallback: cancelCallback, unstable_now: now, unstable_getFirstCallbackNode: getFirstCallbackNode, unstable_pauseExecution: pauseExecution, unstable_continueExecution: continueExecution, unstable_forceFrameRate: forceFrameRate, unstable_shouldYield: shouldYield, unstable_NormalPriority: NormalPriority, } = Scheduler; function displayTestResult(testNumber) { const expectationNode = document.getElementById('test-' + testNumber + '-expected'); const resultNode = document.getElementById('test-' + testNumber); resultNode.innerHTML = latestResults[testNumber - 1].join('<br />'); expectationNode.innerHTML = expectedResults[testNumber - 1].join('<br />'); } function clearTestResult(testNumber) { const resultNode = document.getElementById('test-' + testNumber); resultNode.innerHTML = ''; latestResults[testNumber - 1] = []; } function updateTestResult(testNumber, textToAddToResult) { latestResults[testNumber - 1].push(textToAddToResult); }; function checkTestResult(testNumber) { let correct = true; const expected = expectedResults[testNumber - 1]; // zero indexing const result = latestResults[testNumber - 1]; // zero indexing if (expected.length !== result.length) { correct = false; } else { for (let i = 0, len = expected.length; i < len; i++) { if (expected[i] !== result[i]) { correct = false; break; } } } const currentClass = correct ? 'correct' : 'incorrect'; const previousClass = correct ? 'incorrect' : 'correct'; document.getElementById('test-' + testNumber).classList.remove(previousClass); document.getElementById('test-' + testNumber).classList.add(currentClass); } function logWhenFramesStart(testNumber, cb) { requestAnimationFrame(() => { updateTestResult(testNumber, 'frame 1 started'); requestAnimationFrame(() => { updateTestResult(testNumber, 'frame 2 started'); requestAnimationFrame(() => { updateTestResult(testNumber, 'frame 3 started... we stop counting now.'); cb(); }); }); }); } // push in results when we run the test const latestResults = [ // test 1 [ ], // test 2 [ ], // test 3 [ ], // test 4 [ ], // test 5 [ ], ]; const expectedResults = [ // test 1 [ 'scheduled Cb1', 'frame 1 started', 'cb1 called with argument of false', 'frame 2 started', 'frame 3 started... we stop counting now.', ], // test 2 [ 'scheduled CbA', 'scheduled CbB', 'frame 1 started', 'cbA called with argument of false', 'cbB called with argument of false', 'frame 2 started', 'frame 3 started... we stop counting now.', ], // test 3 [ 'scheduled CbA', 'scheduled CbB', 'frame 1 started', 'scheduled CbA again', 'cbA0 called with argument of false', 'cbB called with argument of false', 'cbA1 called with argument of false', 'frame 2 started', 'frame 3 started... we stop counting now.', ], // test 4 [ 'scheduled cbA', 'scheduled cbB', 'scheduled cbC', 'scheduled cbD', 'frame 1 started', 'cbC called with argument of {"didTimeout":true}', 'cbA called with argument of false', 'cbA running and taking some time', 'frame 2 started', 'cbB called with argument of false', 'cbD called with argument of false', 'frame 3 started... we stop counting now.', ], // test 5 [ // ... TODO ], [], [], // Test 8 [ 'Queue size: 0.', 'Pausing... press continue to resume.', 'Queue size: 2.', 'Finishing...', 'Done!', ], // test 9 [ 'Forcing new frame times...', 'Using new frame time!', 'Using new frame time!', 'Finished!', ], // test 10 [ 'Running work for 10 seconds...', 'Ran scheduled work for >99% of the time.', ], // test 11 [ 'Running work for 10 seconds...', 'Ran scheduled work for >95% of the time.', ], ]; function runTestOne() { // Test 1 // Calls the callback with the frame when not blocked clearTestResult(1); const test1Log = []; const cb1Arguments = []; const cb1 = (x) => { updateTestResult(1, 'cb1 called with argument of ' + JSON.stringify(x)); } scheduleCallback(NormalPriority, cb1); updateTestResult(1, 'scheduled Cb1'); logWhenFramesStart(1, () => { displayTestResult(1); checkTestResult(1); }); }; function runTestTwo() { // Test 2 // accepts multiple callbacks and calls within frame when not blocked clearTestResult(2); const cbA = (x) => { updateTestResult(2, 'cbA called with argument of ' + JSON.stringify(x)); } const cbB = (x) => { updateTestResult(2, 'cbB called with argument of ' + JSON.stringify(x)); } scheduleCallback(NormalPriority, cbA); updateTestResult(2, 'scheduled CbA'); scheduleCallback(NormalPriority, cbB); updateTestResult(2, 'scheduled CbB'); logWhenFramesStart(2, () => { displayTestResult(2); checkTestResult(2); }); } function runTestThree() { // Test 3 // Schedules callbacks in correct order when they use scheduleCallback to schedule themselves clearTestResult(3); let callbackAIterations = 0; const cbA = (x) => { if (callbackAIterations < 1) { scheduleCallback(NormalPriority, cbA); updateTestResult(3, 'scheduled CbA again'); } updateTestResult(3, 'cbA' + callbackAIterations + ' called with argument of ' + JSON.stringify(x)); callbackAIterations++; } const cbB = (x) => { updateTestResult(3, 'cbB called with argument of ' + JSON.stringify(x)); } scheduleCallback(NormalPriority, cbA); updateTestResult(3, 'scheduled CbA'); scheduleCallback(NormalPriority, cbB); updateTestResult(3, 'scheduled CbB'); logWhenFramesStart(3, () => { displayTestResult(3); checkTestResult(3); }); } function waitForTimeToPass(timeInMs) { const startTime = Date.now(); const endTime = startTime + timeInMs; while (Date.now() < endTime) { // wait... } } function runTestFour() { // Test 4 // Calls timed out callbacks and then any more pending callbacks, defers others if time runs out clearTestResult(4); const cbA = (x) => { updateTestResult(4, 'cbA called with argument of ' + JSON.stringify(x)); updateTestResult(4, 'cbA running and taking some time'); waitForTimeToPass(35); } const cbB = (x) => { updateTestResult(4, 'cbB called with argument of ' + JSON.stringify(x)); } const cbC = (x) => { updateTestResult(4, 'cbC called with argument of ' + JSON.stringify(x)); } const cbD = (x) => { updateTestResult(4, 'cbD called with argument of ' + JSON.stringify(x)); } scheduleCallback(NormalPriority, cbA); // won't time out updateTestResult(4, 'scheduled cbA'); scheduleCallback(NormalPriority, cbB, {timeout: 100}); // times out later updateTestResult(4, 'scheduled cbB'); scheduleCallback(NormalPriority, cbC, {timeout: 1}); // will time out fast updateTestResult(4, 'scheduled cbC'); scheduleCallback(NormalPriority, cbD); // won't time out updateTestResult(4, 'scheduled cbD'); // should have run in order of C, A, B, D logWhenFramesStart(4, () => { displayTestResult(4); checkTestResult(4); }); } // Error handling function runTestFive() { // Test 5 // When some callbacks throw errors, still calls them all within the same frame const cbA = (x) => { console.log('cbA called with argument of ' + JSON.stringify(x)); } const cbB = (x) => { console.log('cbB called with argument of ' + JSON.stringify(x)); console.log('cbB is about to throw an error!'); throw new Error('error B'); } const cbC = (x) => { console.log('cbC called with argument of ' + JSON.stringify(x)); } const cbD = (x) => { console.log('cbD called with argument of ' + JSON.stringify(x)); console.log('cbD is about to throw an error!'); throw new Error('error D'); } const cbE = (x) => { console.log('cbE called with argument of ' + JSON.stringify(x)); console.log('This was the last callback! ------------------'); } console.log('We are aiming to roughly emulate the way ' + '`requestAnimationFrame` handles errors from callbacks.'); console.log('about to run the simulation of what it should look like...:'); requestAnimationFrame(() => { console.log('frame 1 started'); requestAnimationFrame(() => { console.log('frame 2 started'); requestAnimationFrame(() => { console.log('frame 3 started... we stop counting now.'); console.log('about to wait a moment and start this again but ' + 'with the scheduler instead of requestAnimationFrame'); setTimeout(runSchedulerCode, 1000); }); }); }); requestAnimationFrame(cbA); console.log('scheduled cbA'); requestAnimationFrame(cbB); // will throw error console.log('scheduled cbB'); requestAnimationFrame(cbC); console.log('scheduled cbC'); requestAnimationFrame(cbD); // will throw error console.log('scheduled cbD'); requestAnimationFrame(cbE); console.log('scheduled cbE'); function runSchedulerCode() { console.log('-------------------------------------------------------------'); console.log('now lets see what it looks like using the scheduler...:'); requestAnimationFrame(() => { console.log('frame 1 started'); requestAnimationFrame(() => { console.log('frame 2 started'); requestAnimationFrame(() => { console.log('frame 3 started... we stop counting now.'); }); }); }); scheduleCallback(NormalPriority, cbA); console.log('scheduled cbA'); scheduleCallback(NormalPriority, cbB); // will throw error console.log('scheduled cbB'); scheduleCallback(NormalPriority, cbC); console.log('scheduled cbC'); scheduleCallback(NormalPriority, cbD); // will throw error console.log('scheduled cbD'); scheduleCallback(NormalPriority, cbE); console.log('scheduled cbE'); }; } function runTestSix() { // Test 6 // When some callbacks throw errors, still calls them all within the same frame const cbA = (x) => { console.log('cbA called with argument of ' + JSON.stringify(x)); console.log('cbA is about to throw an error!'); throw new Error('error A'); } const cbB = (x) => { console.log('cbB called with argument of ' + JSON.stringify(x)); } const cbC = (x) => { console.log('cbC called with argument of ' + JSON.stringify(x)); } const cbD = (x) => { console.log('cbD called with argument of ' + JSON.stringify(x)); console.log('cbD is about to throw an error!'); throw new Error('error D'); } const cbE = (x) => { console.log('cbE called with argument of ' + JSON.stringify(x)); console.log('This was the last callback! ------------------'); } console.log('We are aiming to roughly emulate the way ' + '`requestAnimationFrame` handles errors from callbacks.'); console.log('about to run the simulation of what it should look like...:'); requestAnimationFrame(() => { console.log('frame 1 started'); requestAnimationFrame(() => { console.log('frame 2 started'); requestAnimationFrame(() => { console.log('frame 3 started... we stop counting now.'); console.log('about to wait a moment and start this again but ' + 'with the scheduler instead of requestAnimationFrame'); setTimeout(runSchedulerCode, 1000); }); }); }); requestAnimationFrame(cbC); console.log('scheduled cbC first; simulating timing out'); requestAnimationFrame(cbD); // will throw error console.log('scheduled cbD first; simulating timing out'); requestAnimationFrame(cbE); console.log('scheduled cbE first; simulating timing out'); requestAnimationFrame(cbA); console.log('scheduled cbA'); // will throw error requestAnimationFrame(cbB); console.log('scheduled cbB'); function runSchedulerCode() { console.log('-------------------------------------------------------------'); console.log('now lets see what it looks like using the scheduler...:'); requestAnimationFrame(() => { console.log('frame 1 started'); requestAnimationFrame(() => { console.log('frame 2 started'); requestAnimationFrame(() => { console.log('frame 3 started... we stop counting now.'); }); }); }); scheduleCallback(NormalPriority, cbA); console.log('scheduled cbA'); scheduleCallback(NormalPriority, cbB); // will throw error console.log('scheduled cbB'); scheduleCallback(NormalPriority, cbC, {timeout: 1}); console.log('scheduled cbC'); scheduleCallback(NormalPriority, cbD, {timeout: 1}); // will throw error console.log('scheduled cbD'); scheduleCallback(NormalPriority, cbE, {timeout: 1}); console.log('scheduled cbE'); }; } function runTestSeven() { // Test 7 // Calls callbacks, continues calling them even when this tab is in the // background clearTestResult(7); let counter = -1; function incrementCounterAndScheduleNextCallback() { const counterNode = document.getElementById('test-7'); counter++; counterNode.innerHTML = counter; waitForTimeToPass(100); scheduleCallback(NormalPriority, incrementCounterAndScheduleNextCallback); } scheduleCallback(NormalPriority, incrementCounterAndScheduleNextCallback); } function runTestEight() { // Test 8 // Pauses execution, dumps the queue, and continues execution clearTestResult(8); function countNodesInStack(firstCallbackNode) { var node = firstCallbackNode; var count = 0; if (node !== null) { do { count = count + 1; node = node.next; } while (node !== firstCallbackNode); } return count; } scheduleCallback(NormalPriority, () => { // size should be 0 updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`); updateTestResult(8, 'Pausing... press continue to resume.'); pauseExecution(); scheduleCallback(NormalPriority, function () { updateTestResult(8, 'Finishing...'); displayTestResult(8); }) scheduleCallback(NormalPriority, function () { updateTestResult(8, 'Done!'); displayTestResult(8); checkTestResult(8); }) // new size should be 2 now updateTestResult(8, `Queue size: ${countNodesInStack(getFirstCallbackNode())}.`); displayTestResult(8); }); } function continueTestEight() { continueExecution(); } function runTestNine() { clearTestResult(9); // We have this to make sure that the thing that goes right after it can get a full frame var forceFrameFinish = () => { while (!shouldYield()) { waitForTimeToPass(1); } waitForTimeToPass(100); } scheduleCallback(NormalPriority, forceFrameFinish); scheduleCallback(NormalPriority, () => { var startTime = now(); while (!shouldYield()) {} var initialFrameTime = now() - startTime; var newFrameTime = (initialFrameTime * 2) > 60 ? (initialFrameTime * 2) : 60; var newFrameRate = Math.floor(1000/newFrameTime); updateTestResult(9, `Forcing new frame times...`); displayTestResult(9); forceFrameRate(newFrameRate); var toSchedule = (again) => { var startTime = now(); while (!shouldYield()) {} var frameTime = now() - startTime; if (frameTime >= (newFrameTime-8)) { updateTestResult(9, `Using new frame time!`); } else { updateTestResult(9, `Failed to use new frame time. (off by ${newFrameTime - frameTime}ms)`); } displayTestResult(9); if (again) { scheduleCallback(NormalPriority, forceFrameFinish); scheduleCallback(NormalPriority, () => {toSchedule(false);}); } else { updateTestResult(9, `Finished!`); forceFrameRate(0); displayTestResult(9); checkTestResult(9); } } scheduleCallback(NormalPriority, forceFrameFinish); scheduleCallback(NormalPriority, () => {toSchedule(true);}); }); } function runTestTen() { clearTestResult(10); updateTestResult(10, `Running work for 10 seconds...`); var testStartTime = now(); var accumulatedWork = 0 function loop() { var startTime = now(); while (!shouldYield()) {} var endTime = now(); accumulatedWork += endTime - startTime; var runTime = endTime - testStartTime; if (runTime > 10000) { updateTestResult(10, `Ran scheduled work for ${(100 * accumulatedWork / runTime).toFixed(2)}% of the time.`); displayTestResult(10); return; } scheduleCallback(NormalPriority, loop); } scheduleCallback(NormalPriority, loop); } function runTestEleven() { clearTestResult(11); updateTestResult(11, `Running work for 10 seconds...`); var testStartTime = now(); var lastInsertion = 0; var accumulatedWork = 0 function loop() { var startTime = now(); var timeSinceLastDOMInteraction = startTime - lastInsertion; if (timeSinceLastDOMInteraction > 15) { lastInsertion = startTime; var node = document.createElement('div'); node.textContent = startTime; document.body.appendChild(node); document.body.clientHeight; // force layout } while (!shouldYield()) {} var endTime = now(); accumulatedWork += endTime - startTime; var runTime = endTime - testStartTime; if (runTime > 10000) { updateTestResult(11, `Ran scheduled work for ${(100 * accumulatedWork / runTime).toFixed(2)}% of the time.`); displayTestResult(11); return; } scheduleCallback(NormalPriority, loop); } scheduleCallback(NormalPriority, loop); } </script type="text/babel"> </body> </html>