diff options
Diffstat (limited to 'devtools/server/tracer')
4 files changed, 560 insertions, 44 deletions
diff --git a/devtools/server/tracer/tests/browser/browser_document_tracer.js b/devtools/server/tracer/tests/browser/browser_document_tracer.js index 694842fa8b..dcf2c9eb4d 100644 --- a/devtools/server/tracer/tests/browser/browser_document_tracer.js +++ b/devtools/server/tracer/tests/browser/browser_document_tracer.js @@ -52,7 +52,7 @@ add_task(async function testTracingWorker() { const firstFrame = frames[0]; is(firstFrame.formatedDisplayName, "λ foo"); - is(firstFrame.currentDOMEvent, "DOM(click)"); + is(firstFrame.currentDOMEvent, "DOM | click"); const lastFrame = frames.at(-1); is(lastFrame.formatedDisplayName, "λ bar"); diff --git a/devtools/server/tracer/tests/browser/browser_worker_tracer.js b/devtools/server/tracer/tests/browser/browser_worker_tracer.js index 815da85853..f555bd06b0 100644 --- a/devtools/server/tracer/tests/browser/browser_worker_tracer.js +++ b/devtools/server/tracer/tests/browser/browser_worker_tracer.js @@ -52,7 +52,7 @@ add_task(async function testTracingWorker() { ok(lastFrame.frame); }); -function waitForWorkerDebugger(url, dbgUrl) { +function waitForWorkerDebugger(url) { return new Promise(function (resolve) { wdm.addListener({ onRegister(dbg) { diff --git a/devtools/server/tracer/tests/xpcshell/test_tracer.js b/devtools/server/tracer/tests/xpcshell/test_tracer.js index fe9a984aa8..0f38052ba5 100644 --- a/devtools/server/tracer/tests/xpcshell/test_tracer.js +++ b/devtools/server/tracer/tests/xpcshell/test_tracer.js @@ -238,3 +238,267 @@ add_task(async function testTracingFunctionReturnAndValues() { info("Stop tracing"); stopTracing(); }); + +add_task(async function testTracingStep() { + // Test the `traceStep` flag + const sandbox = Cu.Sandbox("https://example.com"); + const source = ` +function foo() { + bar(); /* line 3 */ + second(); /* line 4 */ +} +function bar() { + let res; /* line 7 */ + if (1 === 1) { /* line 8 */ + res = "string"; /* line 9 */ + } else { + res = "nope" + } + return res; /* line 13 */ +}; +function second() { + let x = 0; /* line 16 */ + for (let i = 0; i < 2; i++) { /* line 17 */ + x++; /* line 18 */ + } + return null; /* line 20 */ +}; +foo();`; + Cu.evalInSandbox(source, sandbox, null, "file.js", 1); + + // Pass an override method to catch all strings tentatively logged to stdout + const logs = []; + function loggingMethod(str) { + logs.push(str); + } + + info("Start tracing"); + startTracing({ + global: sandbox, + traceSteps: true, + loggingMethod, + }); + + info("Call some code"); + sandbox.foo(); + + Assert.equal(logs.length, 19); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ foo"); + Assert.stringContains(logs[1], "file.js:3:3"); + + // Each "step" only prints the location and nothing more + Assert.stringContains(logs[2], "file.js:3:3"); + + Assert.stringContains(logs[3], "λ bar"); + Assert.stringContains(logs[3], "file.js:6:16"); + + Assert.stringContains(logs[4], "file.js:8:7"); + + Assert.stringContains(logs[5], "file.js:9:5"); + + Assert.stringContains(logs[6], "file.js:13:3"); + + Assert.stringContains(logs[7], "file.js:4:3"); + + Assert.stringContains(logs[8], "λ second"); + Assert.stringContains(logs[8], "file.js:15:19"); + + Assert.stringContains(logs[9], "file.js:16:11"); + + // For loop + Assert.stringContains(logs[10], "file.js:17:16"); + + Assert.stringContains(logs[11], "file.js:17:19"); + + Assert.stringContains(logs[12], "file.js:18:5"); + + Assert.stringContains(logs[13], "file.js:17:26"); + + Assert.stringContains(logs[14], "file.js:17:19"); + + Assert.stringContains(logs[15], "file.js:18:5"); + + Assert.stringContains(logs[16], "file.js:17:26"); + + Assert.stringContains(logs[17], "file.js:17:19"); + // End of for loop + + Assert.stringContains(logs[18], "file.js:20:3"); + + info("Stop tracing"); + stopTracing(); +}); + +add_task(async function testTracingPauseOnStep() { + // Test the `pauseOnStep` flag + const sandbox = Cu.Sandbox("https://example.com"); + sandbox.dump = dump; + const source = `var counter = 0; function incrementCounter() { let x = 0; dump("++\\n"); counter++; };`; + Cu.evalInSandbox(source, sandbox); + + // Pass an override method to catch all strings tentatively logged to stdout + const logs = []; + let loggingMethodResolve; + function loggingMethod(str) { + logs.push(str); + if (loggingMethodResolve) { + loggingMethodResolve(); + } + } + + info("Start tracing without pause"); + startTracing({ + global: sandbox, + loggingMethod, + }); + + info("Call some code"); + sandbox.incrementCounter(); + + Assert.equal(logs.length, 2); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ incrementCounter"); + + info( + "When pauseOnStep isn't used, the traced code runs synchronously to completion" + ); + Assert.equal(sandbox.counter, 1); + + info("Stop tracing"); + stopTracing(); + + logs.length = 0; + sandbox.counter = 0; + + info("Start tracing with 0ms pause"); + startTracing({ + global: sandbox, + pauseOnStep: 0, + loggingMethod, + }); + + let onTraces = Promise.withResolvers(); + let onResumed = Promise.withResolvers(); + // This is used when receiving new traces in `loggingMethod()` + loggingMethodResolve = onTraces.resolve; + + info( + "Run the to-be-traced code in a distinct event loop as it would be paused synchronously and would prevent further test script execution" + ); + Services.tm.dispatchToMainThread(() => { + sandbox.incrementCounter(); + onResumed.resolve(); + }); + + info("Wait for tracer to call the listener"); + await onTraces.promise; + + Assert.equal(logs.length, 2); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ incrementCounter"); + + info( + "When pauseInStep is used, the tracer listener is called, but the traced function is paused and doesn't run synchronously to completion" + ); + Assert.equal( + sandbox.counter, + 0, + "The increment method was called but its execution flow was blocked and couldn't increment" + ); + + info("Wait for traced code to be resumed"); + await onResumed.promise; + info( + "If we release the event loop, we can see the traced function completion" + ); + Assert.equal(sandbox.counter, 1); + + info("Stop tracing"); + stopTracing(); + + logs.length = 0; + sandbox.counter = 0; + + info("Start tracing with 250ms pause"); + startTracing({ + global: sandbox, + pauseOnStep: 250, + loggingMethod, + }); + + onTraces = Promise.withResolvers(); + onResumed = Promise.withResolvers(); + // This is used when receiving new traces in `loggingMethod()` + loggingMethodResolve = onTraces.resolve; + + info( + "Run the to-be-traced code in a distinct event loop as it would be paused synchronously and would prevent further test script execution" + ); + const startTimestamp = Cu.now(); + Services.tm.dispatchToMainThread(() => { + sandbox.incrementCounter(); + onResumed.resolve(); + }); + + info("Wait for tracer to call the listener"); + await onTraces.promise; + + Assert.equal(logs.length, 2); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ incrementCounter"); + + info( + "When pauseInStep is used, the tracer lsitener is called, but the traced function is paused and doesn't run synchronously to completion" + ); + Assert.equal(sandbox.counter, 0); + + info("Wait for traced code to be resumed"); + await onResumed.promise; + info( + "If we release the event loop, we can see the traced function completion" + ); + Assert.equal(sandbox.counter, 1); + info("The thread should have paused at least the pauseOnStep's duration"); + Assert.greater(Cu.now() - startTimestamp, 250); + + info("Stop tracing"); + stopTracing(); +}); + +add_task(async function testTracingFilterSourceUrl() { + // Test the `filterFrameSourceUrl` flag + const sandbox = Cu.Sandbox("https://example.com"); + + // Use a unique global (sandbox), but with two distinct scripts (first.js and second.js) + const source1 = `function foo() { bar(); }`; + Cu.evalInSandbox(source1, sandbox, null, "first.js", 1); + + // Only code running in that second source should be traced. + const source2 = `function bar() { }`; + Cu.evalInSandbox(source2, sandbox, null, "second.js", 1); + + // Pass an override method to catch all strings tentatively logged to stdout + const logs = []; + function loggingMethod(str) { + logs.push(str); + } + + info("Start tracing"); + startTracing({ + global: sandbox, + filterFrameSourceUrl: "second", + loggingMethod, + }); + + info("Call some code"); + sandbox.foo(); + + Assert.equal(logs.length, 2); + Assert.equal(logs[0], "Start tracing JavaScript\n"); + Assert.stringContains(logs[1], "λ bar"); + Assert.stringContains(logs[1], "second.js:1:18"); + + info("Stop tracing"); + stopTracing(); +}); diff --git a/devtools/server/tracer/tracer.jsm b/devtools/server/tracer/tracer.jsm index 82c746bb57..955b25fe3a 100644 --- a/devtools/server/tracer/tracer.jsm +++ b/devtools/server/tracer/tracer.jsm @@ -25,6 +25,7 @@ const EXPORTED_SYMBOLS = [ "addTracingListener", "removeTracingListener", "NEXT_INTERACTION_MESSAGE", + "DOM_MUTATIONS", ]; const NEXT_INTERACTION_MESSAGE = @@ -43,8 +44,23 @@ const FRAME_EXIT_REASONS = { THROW: "throw", }; +const DOM_MUTATIONS = { + // Track all DOM Node being added + ADD: "add", + // Track all attributes being modified + ATTRIBUTES: "attributes", + // Track all DOM Node being removed + REMOVE: "remove", +}; + const listeners = new Set(); +// Detecting worker is different if this file is loaded via Common JS loader (isWorker global) +// or as a JSM (constructor name) +const isWorker = + globalThis.isWorker || + globalThis.constructor.name == "WorkerDebuggerGlobalScope"; + // This module can be loaded from the worker thread, where we can't use ChromeUtils. // So implement custom lazy getters (without XPCOMUtils ESM) from here. // Worker codepath in DevTools will pass a custom Debugger instance. @@ -60,7 +76,7 @@ const customLazy = { // (ex: from tracer actor module), // this module no longer has WorkerDebuggerGlobalScope as global, // but has to use require() to pull Debugger. - if (typeof isWorker == "boolean") { + if (isWorker) { return require("Debugger"); } const { addDebuggerToGlobal } = ChromeUtils.importESModule( @@ -113,25 +129,44 @@ const customLazy = { * @param {Boolean} options.traceDOMEvents * Optional setting to enable tracing all the DOM events being going through * dom/events/EventListenerManager.cpp's `EventListenerManager`. + * @param {Array<string>} options.traceDOMMutations + * Optional setting to enable tracing all the DOM mutations. + * This array may contains three strings: + * - "add": trace all new DOM Node being added, + * - "attributes": trace all DOM attribute modifications, + * - "delete": trace all DOM Node being removed. * @param {Boolean} options.traceValues * Optional setting to enable tracing all function call values as well, * as returned values (when we do log returned frames). * @param {Boolean} options.traceOnNextInteraction * Optional setting to enable when the tracing should only start when the * use starts interacting with the page. i.e. on next keydown or mousedown. + * @param {Boolean} options.traceSteps + * Optional setting to enable tracing each frame within a function execution. + * (i.e. not only function call and function returns [when traceFunctionReturn is true]) * @param {Boolean} options.traceFunctionReturn * Optional setting to enable when the tracing should notify about frame exit. * i.e. when a function call returns or throws. + * @param {String} options.filterFrameSourceUrl + * Optional setting to restrict all traces to only a given source URL. + * This is a loose check, so any source whose URL includes the passed string will be traced. * @param {Number} options.maxDepth * Optional setting to ignore frames when depth is greater than the passed number. * @param {Number} options.maxRecords * Optional setting to stop the tracer after having recorded at least * the passed number of top level frames. + * @param {Number} options.pauseOnStep + * Optional setting to delay each frame execution for a given amount of time in ms. */ class JavaScriptTracer { constructor(options) { this.onEnterFrame = this.onEnterFrame.bind(this); + // DevTools CommonJS Workers modules don't have access to AbortController + if (!isWorker) { + this.abortController = new AbortController(); + } + // By default, we would trace only JavaScript related to caller's global. // As there is no way to compute the caller's global default to the global of the // mandatory options argument. @@ -152,56 +187,48 @@ class JavaScriptTracer { if (!this.loggingMethod) { // On workers, `dump` can't be called with JavaScript on another object, // so bind it. - // Detecting worker is different if this file is loaded via Common JS loader (isWorker) - // or as a JSM (constructor name) - this.loggingMethod = - typeof isWorker == "boolean" || - globalThis.constructor.name == "WorkerDebuggerGlobalScope" - ? dump.bind(null) - : dump; + this.loggingMethod = isWorker ? dump.bind(null) : dump; } this.traceDOMEvents = !!options.traceDOMEvents; + + if (options.traceDOMMutations) { + if (!Array.isArray(options.traceDOMMutations)) { + throw new Error("'traceDOMMutations' attribute should be an array"); + } + const acceptedValues = Object.values(DOM_MUTATIONS); + if (!options.traceDOMMutations.every(e => acceptedValues.includes(e))) { + throw new Error( + `'traceDOMMutations' only accept array of strings whose values can be: ${acceptedValues}` + ); + } + this.traceDOMMutations = options.traceDOMMutations; + } + this.traceSteps = !!options.traceSteps; this.traceValues = !!options.traceValues; this.traceFunctionReturn = !!options.traceFunctionReturn; this.maxDepth = options.maxDepth; this.maxRecords = options.maxRecords; this.records = 0; + if ("pauseOnStep" in options) { + if (typeof options.pauseOnStep != "number") { + throw new Error("'pauseOnStep' attribute should be a number"); + } + this.pauseOnStep = options.pauseOnStep; + } + if ("filterFrameSourceUrl" in options) { + if (typeof options.filterFrameSourceUrl != "string") { + throw new Error("'filterFrameSourceUrl' attribute should be a string"); + } + this.filterFrameSourceUrl = options.filterFrameSourceUrl; + } // An increment used to identify function calls and their returned/exit frames this.frameId = 0; // This feature isn't supported on Workers as they aren't involving user events - if (options.traceOnNextInteraction && typeof isWorker !== "boolean") { - this.abortController = new AbortController(); - const listener = () => { - this.abortController.abort(); - // Avoid tracing if the users asked to stop tracing. - if (this.dbg) { - this.#startTracing(); - } - }; - const eventOptions = { - signal: this.abortController.signal, - capture: true, - }; - // Register the event listener on the Chrome Event Handler in order to receive the event first. - // When used for the parent process target, `tracedGlobal` is browser.xhtml's window, which doesn't have a chromeEventHandler. - const eventHandler = - this.tracedGlobal.docShell.chromeEventHandler || this.tracedGlobal; - eventHandler.addEventListener("mousedown", listener, eventOptions); - eventHandler.addEventListener("keydown", listener, eventOptions); - - // Significate to the user that the tracer is registered, but not tracing just yet. - let shouldLogToStdout = listeners.size == 0; - for (const l of listeners) { - if (typeof l.onTracingPending == "function") { - shouldLogToStdout |= l.onTracingPending(); - } - } - if (shouldLogToStdout) { - this.loggingMethod(this.prefix + NEXT_INTERACTION_MESSAGE + "\n"); - } + if (options.traceOnNextInteraction && !isWorker) { + this.#waitForNextInteraction(); } else { this.#startTracing(); } @@ -212,6 +239,44 @@ class JavaScriptTracer { isTracing = false; /** + * In case `traceOnNextInteraction` option is used, delay the actual start of tracing until a first user interaction. + */ + #waitForNextInteraction() { + // Use a dedicated Abort Controller as we are going to stop it as soon as we get the first user interaction, + // whereas other listeners would typically wait for tracer stop. + this.nextInteractionAbortController = new AbortController(); + + const listener = () => { + this.nextInteractionAbortController.abort(); + // Avoid tracing if the users asked to stop tracing while we were waiting for the user interaction. + if (this.dbg) { + this.#startTracing(); + } + }; + const eventOptions = { + signal: this.nextInteractionAbortController.signal, + capture: true, + }; + // Register the event listener on the Chrome Event Handler in order to receive the event first. + // When used for the parent process target, `tracedGlobal` is browser.xhtml's window, which doesn't have a chromeEventHandler. + const eventHandler = + this.tracedGlobal.docShell.chromeEventHandler || this.tracedGlobal; + eventHandler.addEventListener("mousedown", listener, eventOptions); + eventHandler.addEventListener("keydown", listener, eventOptions); + + // Significate to the user that the tracer is registered, but not tracing just yet. + let shouldLogToStdout = listeners.size == 0; + for (const l of listeners) { + if (typeof l.onTracingPending == "function") { + shouldLogToStdout |= l.onTracingPending(); + } + } + if (shouldLogToStdout) { + this.loggingMethod(this.prefix + NEXT_INTERACTION_MESSAGE + "\n"); + } + } + + /** * Actually really start watching for executions. * * This may be delayed when traceOnNextInteraction options is used. @@ -225,6 +290,10 @@ class JavaScriptTracer { if (this.traceDOMEvents) { this.startTracingDOMEvents(); } + // This feature isn't supported on Workers as they aren't interacting with the DOM Tree + if (this.traceDOMMutations?.length > 0 && !isWorker) { + this.startTracingDOMMutations(); + } // In any case, we consider the tracing as started this.notifyToggle(true); @@ -248,6 +317,97 @@ class JavaScriptTracer { this.currentDOMEvent = null; } + startTracingDOMMutations() { + this.tracedGlobal.document.devToolsWatchingDOMMutations = true; + + const eventOptions = { + signal: this.abortController.signal, + capture: true, + }; + if (this.traceDOMMutations.includes(DOM_MUTATIONS.ADD)) { + this.tracedGlobal.docShell.chromeEventHandler.addEventListener( + "devtoolschildinserted", + this.#onDOMMutation, + eventOptions + ); + } + if (this.traceDOMMutations.includes(DOM_MUTATIONS.ATTRIBUTES)) { + this.tracedGlobal.docShell.chromeEventHandler.addEventListener( + "devtoolsattrmodified", + this.#onDOMMutation, + eventOptions + ); + } + if (this.traceDOMMutations.includes(DOM_MUTATIONS.REMOVE)) { + this.tracedGlobal.docShell.chromeEventHandler.addEventListener( + "devtoolschildremoved", + this.#onDOMMutation, + eventOptions + ); + } + } + + stopTracingDOMMutations() { + this.tracedGlobal.document.devToolsWatchingDOMMutations = false; + // Note that the event listeners are all going to be unregistered via the AbortController. + } + + /** + * Called for any DOM Mutation done in the traced document. + * + * @param {DOM Event} event + */ + #onDOMMutation = event => { + // Ignore elements inserted by DevTools, like the inspector's highlighters + if (event.target.isNativeAnonymous) { + return; + } + + let type = ""; + switch (event.type) { + case "devtoolschildinserted": + type = DOM_MUTATIONS.ADD; + break; + case "devtoolsattrmodified": + type = DOM_MUTATIONS.ATTRIBUTES; + break; + case "devtoolschildremoved": + type = DOM_MUTATIONS.REMOVE; + break; + default: + throw new Error("Unexpected DOM Mutation event type: " + event.type); + } + + let shouldLogToStdout = true; + if (listeners.size > 0) { + shouldLogToStdout = false; + for (const listener of listeners) { + // If any listener return true, also log to stdout + if (typeof listener.onTracingDOMMutation == "function") { + shouldLogToStdout |= listener.onTracingDOMMutation({ + depth: this.depth, + prefix: this.prefix, + + type, + element: event.target, + caller: Components.stack.caller, + }); + } + } + } + + if (shouldLogToStdout) { + const padding = "—".repeat(this.depth + 1); + this.loggingMethod( + this.prefix + + padding + + `[DOM Mutation | ${type}] ` + + objectToString(event.target) + + "\n" + ); + } + }; + /** * Called by DebuggerNotificationObserver interface when a DOM event start being notified * and after it has been notified. @@ -277,7 +437,7 @@ class JavaScriptTracer { .makeDebuggeeValue(notification.event) .getProperty("type").return; } - this.currentDOMEvent = `DOM(${type})`; + this.currentDOMEvent = `DOM | ${type}`; } else { this.currentDOMEvent = notification.type; } @@ -306,14 +466,22 @@ class JavaScriptTracer { this.depth = 0; // Cancel the traceOnNextInteraction event listeners. - if (this.abortController) { - this.abortController.abort(); - this.abortController = null; + if (this.nextInteractionAbortController) { + this.nextInteractionAbortController.abort(); + this.nextInteractionAbortController = null; } if (this.traceDOMEvents) { this.stopTracingDOMEvents(); } + if (this.traceDOMMutations?.length > 0 && !isWorker) { + this.stopTracingDOMMutations(); + } + + // Unregister all event listeners + if (this.abortController) { + this.abortController.abort(); + } this.tracedGlobal = null; this.isTracing = false; @@ -406,10 +574,21 @@ class JavaScriptTracer { return; } try { + // If an optional filter is passed, ignore frames which aren't matching the filter string + if ( + this.filterFrameSourceUrl && + !frame.script.source.url?.includes(this.filterFrameSourceUrl) + ) { + return; + } + // Because of async frame which are popped and entered again on completion of the awaited async task, // we have to compute the depth from the frame. (and can't use a simple increment on enter/decrement on pop). const depth = getFrameDepth(frame); + // Save the current depth for the DOM Mutation handler + this.depth = depth; + // Ignore the frame if we reached the depth limit (if one is provided) if (this.maxDepth && depth >= this.maxDepth) { return; @@ -467,6 +646,39 @@ class JavaScriptTracer { this.logFrameEnteredToStdout(frame, depth); } + if (this.traceSteps) { + frame.onStep = () => { + // Spidermonkey steps on many intermediate positions which don't make sense to the user. + // `isStepStart` is close to each statement start, which is meaningful to the user. + const { isStepStart } = frame.script.getOffsetMetadata(frame.offset); + if (!isStepStart) { + return; + } + + shouldLogToStdout = true; + if (listeners.size > 0) { + shouldLogToStdout = false; + for (const listener of listeners) { + // If any listener return true, also log to stdout + if (typeof listener.onTracingFrameStep == "function") { + shouldLogToStdout |= listener.onTracingFrameStep({ + frame, + depth, + prefix: this.prefix, + }); + } + } + } + if (shouldLogToStdout) { + this.logFrameStepToStdout(frame, depth); + } + // Optionaly pause the frame execution by letting the other event loop to run in between. + if (typeof this.pauseOnStep == "number") { + syncPause(this.pauseOnStep); + } + }; + } + frame.onPop = completion => { // Special case async frames. We are exiting the current frame because of waiting for an async task. // (this is typically a `await foo()` from an async function) @@ -520,6 +732,11 @@ class JavaScriptTracer { this.logFrameExitedToStdout(frame, depth, why, rv); } }; + + // Optionaly pause the frame execution by letting the other event loop to run in between. + if (typeof this.pauseOnStep == "number") { + syncPause(this.pauseOnStep); + } } catch (e) { console.error("Exception while tracing javascript", e); } @@ -576,6 +793,20 @@ class JavaScriptTracer { } /** + * Display to stdout one given frame execution, which represents a step within a function execution. + * + * @param {Debugger.Frame} frame + * @param {Number} depth + */ + logFrameStepToStdout(frame, depth) { + const padding = "—".repeat(depth + 1); + + const message = `${padding}— ${getTerminalHyperLink(frame)}`; + + this.loggingMethod(this.prefix + message + "\n"); + } + + /** * Display to stdout the exit of a given frame execution, which represents a function return. * * @param {Debugger.Frame} frame @@ -787,6 +1018,27 @@ function getTerminalHyperLink(frame) { return `\x1B]8;;${href}\x1B\\${href}\x1B]8;;\x1B\\`; } +/** + * Helper function to synchronously pause the current frame execution + * for a given duration in ms. + * + * @param {Number} duration + */ +function syncPause(duration) { + let freeze = true; + const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback( + () => { + freeze = false; + }, + duration, + Ci.nsITimer.TYPE_ONE_SHOT + ); + Services.tm.spinEventLoopUntil("debugger-slow-motion", function () { + return !freeze; + }); +} + // This JSM may be execute as CommonJS when loaded in the worker thread if (typeof module == "object") { module.exports = { |