test_suspendTimeouts.js (6587B)
1 "use strict"; 2 3 // The debugger uses nsIDOMWindowUtils::suspendTimeouts and ...::resumeTimeouts 4 // to ensure that content event handlers do not run while a JavaScript 5 // invocation is stepping or paused at a breakpoint. If a worker thread sends 6 // messages to the content while the content is paused, those messages must not 7 // run until the JavaScript invocation interrupted by the debugger has completed. 8 // 9 // Bug 1426467 is that calling nsIDOMWindowUtils::resumeTimeouts actually 10 // delivers deferred messages itself, calling the content's 'onmessage' handler. 11 // But the debugger calls suspend/resume around each individual interruption of 12 // the debuggee -- each step, say -- meaning that hitting the "step into" button 13 // causes you to step from the debuggee directly into an onmessage handler, 14 // since the onmessage handler is the next function call the debugger sees. 15 // 16 // In other words, delivering deferred messages from resumeTimeouts, as it is 17 // used by the debugger, breaks the run-to-completion rule. They must not be 18 // delivered until after the JavaScript invocation at hand is complete. That's 19 // what this test checks. 20 // 21 // For this test to detect the bug, the following steps must take place in 22 // order: 23 // 24 // 1) The content page must call suspendTimeouts. 25 // 2) A runnable conveying a message from the worker thread must attempt to 26 // deliver the message, see that the content page has suspended such things, 27 // and hold the message for later delivery. 28 // 3) The content page must call resumeTimeouts. 29 // 30 // In a correct implementation, the message from the worker thread is delivered 31 // only after the main thread returns to the event loop after calling 32 // resumeTimeouts in step 3). In the buggy implementation, the onmessage handler 33 // is called directly from the call to resumeTimeouts, so that the onmessage 34 // handlers run in the midst of whatever JavaScript invocation resumed timeouts 35 // (say, stepping in the debugger), in violation of the run-to-completion rule. 36 // 37 // In this specific bug, the handlers are called from resumeTimeouts, but 38 // really, running them any time before that invocation returns to the main 39 // event loop would be a bug. 40 // 41 // Posting the message and calling resumeTimeouts take place in different 42 // threads, but if 2) and 3) don't occur in that order, the worker's message 43 // will never be delayed and the test will pass spuriously. But the worker 44 // can't communicate with the content page directly, to let it know that it 45 // should proceed with step 3): the purpose of suspendTimeouts is to pause 46 // all such communication. 47 // 48 // So instead, the content page creates a MessageChannel, and passes one 49 // MessagePort to the worker and the other to this mochitest (which has its 50 // own window, separate from the one calling suspendTimeouts). The worker 51 // notifies the mochitest when it has posted the message, and then the 52 // mochitest calls into the content to carry out step 3). 53 54 // To help you follow all the callbacks and event handlers, this code pulls out 55 // event handler functions so that control flows from top to bottom. 56 57 window.onload = function () { 58 // This mochitest is not complete until we call SimpleTest.finish. Don't just 59 // exit as soon as we return to the main event loop. 60 SimpleTest.waitForExplicitFinish(); 61 62 const iframe = document.createElement("iframe"); 63 iframe.src = 64 "http://mochi.test:8888/chrome/devtools/server/tests/chrome/suspendTimeouts_content.html"; 65 iframe.onload = iframe_onload_handler; 66 document.body.appendChild(iframe); 67 68 function iframe_onload_handler() { 69 const content = iframe.contentWindow.wrappedJSObject; 70 71 const windowUtils = iframe.contentWindow.windowUtils; 72 73 // Hand over the suspend and resume functions to the content page, along 74 // with some testing utilities. 75 content.suspendTimeouts = function () { 76 SimpleTest.info("test_suspendTimeouts", "calling suspendTimeouts"); 77 windowUtils.suspendTimeouts(); 78 }; 79 content.resumeTimeouts = function () { 80 windowUtils.resumeTimeouts(); 81 SimpleTest.info("test_suspendTimeouts", "resumeTimeouts called"); 82 }; 83 content.info = function (message) { 84 SimpleTest.info("suspendTimeouts_content.js", message); 85 }; 86 content.ok = SimpleTest.ok; 87 content.finish = finish; 88 89 SimpleTest.info( 90 "Disappointed with National Tautology Day? Well, it is what it is." 91 ); 92 93 // Once the worker has sent a message to its parent (which should get delayed), 94 // it sends us a message directly on this channel. 95 const workerPort = content.create_channel(); 96 workerPort.onmessage = handle_worker_echo; 97 98 // Have content send the worker a message that it should echo back to both 99 // content and us. The echo to content should get delayed; the echo to us 100 // should cause our handle_worker_echo to be called. 101 content.start_worker(); 102 103 function handle_worker_echo({ data }) { 104 info(`mochitest received message from worker: ${data}`); 105 106 // As it turns out, it's not correct to assume that, if the worker posts a 107 // message to its parent via the global `postMessage` function, and then 108 // posts a message to the mochitest via the MessagePort, those two 109 // messages will be delivered in the order they were sent. 110 // 111 // - Messages sent via the worker's global's postMessage go through two 112 // ThrottledEventQueues (one in the worker, and one on the parent), and 113 // eventually find their way into the thread's primary event queue, 114 // which is a PrioritizedEventQueue. 115 // 116 // - Messages sent via a MessageChannel whose ports are owned by different 117 // threads are passed as IPDL messages. 118 // 119 // There's basically no reliable way to ensure that delivery to content 120 // has been attempted and the runnable deferred; there are too many 121 // variables affecting the order in which things are processed. Delaying 122 // for a second is the best I could think of. 123 // 124 // Fortunately, this tactic failing can only cause spurious test passes 125 // (the runnable never gets deferred, so things work by accident), not 126 // spurious failures. Without some kind of trustworthy notification that 127 // the runnable has been deferred, perhaps via some special white-box 128 // testing API, we can't do better. 129 setTimeout(() => { 130 content.resume_timeouts(); 131 }, 1000); 132 } 133 134 function finish() { 135 SimpleTest.info("suspendTimeouts_content.js", "called finish"); 136 SimpleTest.finish(); 137 } 138 } 139 };