summaryrefslogtreecommitdiffstats
path: root/devtools/server/tracer
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/tracer
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/tracer')
-rw-r--r--devtools/server/tracer/moz.build14
-rw-r--r--devtools/server/tracer/tests/browser/Worker.tracer.js10
-rw-r--r--devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js36
-rw-r--r--devtools/server/tracer/tests/browser/browser.ini8
-rw-r--r--devtools/server/tracer/tests/browser/browser_worker_tracer.js68
-rw-r--r--devtools/server/tracer/tests/xpcshell/test_tracer.js139
-rw-r--r--devtools/server/tracer/tests/xpcshell/xpcshell.ini6
-rw-r--r--devtools/server/tracer/tracer.jsm364
8 files changed, 645 insertions, 0 deletions
diff --git a/devtools/server/tracer/moz.build b/devtools/server/tracer/moz.build
new file mode 100644
index 0000000000..0f142e0d17
--- /dev/null
+++ b/devtools/server/tracer/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules("tracer.jsm")
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"]
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "General")
diff --git a/devtools/server/tracer/tests/browser/Worker.tracer.js b/devtools/server/tracer/tests/browser/Worker.tracer.js
new file mode 100644
index 0000000000..60db4545a6
--- /dev/null
+++ b/devtools/server/tracer/tests/browser/Worker.tracer.js
@@ -0,0 +1,10 @@
+"use strict";
+
+/* eslint-disable no-unused-vars */
+
+function bar() {}
+function foo() {
+ bar();
+}
+
+postMessage("evaled");
diff --git a/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js b/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js
new file mode 100644
index 0000000000..bd6e646b3b
--- /dev/null
+++ b/devtools/server/tracer/tests/browser/WorkerDebugger.tracer.js
@@ -0,0 +1,36 @@
+"use strict";
+
+/* global global, loadSubScript */
+
+try {
+ // For some reason WorkerDebuggerGlobalScope.global doesn't expose JS variables
+ // and we can't call via global.foo(). Instead we have to go throught the Debugger API.
+ const dbg = new Debugger(global);
+ const [debuggee] = dbg.getDebuggees();
+
+ /* global startTracing, stopTracing, addTracingListener, removeTracingListener */
+ loadSubScript("resource://devtools/server/tracer/tracer.jsm");
+ const frames = [];
+ const listener = {
+ onTracingFrame(args) {
+ frames.push(args);
+
+ // Return true, to also log the trace to stdout
+ return true;
+ },
+ };
+ addTracingListener(listener);
+ startTracing({ global, prefix: "testWorkerPrefix" });
+
+ debuggee.executeInGlobal("foo()");
+
+ stopTracing();
+ removeTracingListener(listener);
+
+ // Send the frames to the main thread to do the assertions there.
+ postMessage(JSON.stringify(frames));
+} catch (e) {
+ dump(
+ "Exception while running debugger test script: " + e + "\n" + e.stack + "\n"
+ );
+}
diff --git a/devtools/server/tracer/tests/browser/browser.ini b/devtools/server/tracer/tests/browser/browser.ini
new file mode 100644
index 0000000000..fafc59dbef
--- /dev/null
+++ b/devtools/server/tracer/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+
+[browser_worker_tracer.js]
+support-files =
+ Worker.tracer.js
+ WorkerDebugger.tracer.js
diff --git a/devtools/server/tracer/tests/browser/browser_worker_tracer.js b/devtools/server/tracer/tests/browser/browser_worker_tracer.js
new file mode 100644
index 0000000000..815da85853
--- /dev/null
+++ b/devtools/server/tracer/tests/browser/browser_worker_tracer.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].getService(
+ Ci.nsIWorkerDebuggerManager
+);
+
+const BASE_URL =
+ "chrome://mochitests/content/browser/devtools/server/tracer/tests/browser/";
+const WORKER_URL = BASE_URL + "Worker.tracer.js";
+const WORKER_DEBUGGER_URL = BASE_URL + "WorkerDebugger.tracer.js";
+
+add_task(async function testTracingWorker() {
+ const onDbg = waitForWorkerDebugger(WORKER_URL);
+
+ info("Instantiate a regular worker");
+ const worker = new Worker(WORKER_URL);
+ info("Wait for worker to reply back");
+ await new Promise(r => (worker.onmessage = r));
+ info("Wait for WorkerDebugger to be instantiated");
+ const dbg = await onDbg;
+
+ const onDebuggerScriptSentFrames = new Promise(resolve => {
+ const listener = {
+ onMessage(msg) {
+ dbg.removeListener(listener);
+ resolve(JSON.parse(msg));
+ },
+ };
+ dbg.addListener(listener);
+ });
+ info("Evaluate a Worker Debugger test script");
+ dbg.initialize(WORKER_DEBUGGER_URL);
+
+ info("Wait for frames to be notified by the debugger script");
+ const frames = await onDebuggerScriptSentFrames;
+
+ is(frames.length, 3);
+ // There is a third frame which relates to the usage of Debugger.Object.executeInGlobal
+ // which we ignore as that's a test side effect.
+ const lastFrame = frames.at(-1);
+ const beforeLastFrame = frames.at(-2);
+ is(beforeLastFrame.depth, 1);
+ is(beforeLastFrame.formatedDisplayName, "λ foo");
+ is(beforeLastFrame.prefix, "testWorkerPrefix: ");
+ ok(beforeLastFrame.frame);
+ is(lastFrame.depth, 2);
+ is(lastFrame.formatedDisplayName, "λ bar");
+ is(lastFrame.prefix, "testWorkerPrefix: ");
+ ok(lastFrame.frame);
+});
+
+function waitForWorkerDebugger(url, dbgUrl) {
+ return new Promise(function (resolve) {
+ wdm.addListener({
+ onRegister(dbg) {
+ if (dbg.url !== url) {
+ return;
+ }
+ ok(true, "Debugger with url " + url + " should be registered.");
+ wdm.removeListener(this);
+ resolve(dbg);
+ },
+ });
+ });
+}
diff --git a/devtools/server/tracer/tests/xpcshell/test_tracer.js b/devtools/server/tracer/tests/xpcshell/test_tracer.js
new file mode 100644
index 0000000000..4150944d88
--- /dev/null
+++ b/devtools/server/tracer/tests/xpcshell/test_tracer.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { addTracingListener, removeTracingListener, startTracing, stopTracing } =
+ ChromeUtils.import("resource://devtools/server/tracer/tracer.jsm");
+
+add_task(async function () {
+ // Because this test uses evalInSandbox, we need to tweak the following prefs
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+});
+
+add_task(async function testTracingContentGlobal() {
+ const toggles = [];
+ const frames = [];
+ const listener = {
+ onTracingToggled(state) {
+ toggles.push(state);
+ },
+ onTracingFrame(frameInfo) {
+ frames.push(frameInfo);
+ },
+ };
+
+ info("Register a tracing listener");
+ addTracingListener(listener);
+
+ const sandbox = Cu.Sandbox("https://example.com");
+ Cu.evalInSandbox("function bar() {}; function foo() {bar()};", sandbox);
+
+ info("Start tracing");
+ startTracing({ global: sandbox, prefix: "testContentPrefix" });
+ Assert.equal(toggles.length, 1);
+ Assert.equal(toggles[0], true);
+
+ info("Call some code");
+ sandbox.foo();
+
+ Assert.equal(frames.length, 2);
+ const lastFrame = frames.pop();
+ const beforeLastFrame = frames.pop();
+ Assert.equal(beforeLastFrame.depth, 0);
+ Assert.equal(beforeLastFrame.formatedDisplayName, "λ foo");
+ Assert.equal(beforeLastFrame.prefix, "testContentPrefix: ");
+ Assert.ok(beforeLastFrame.frame);
+ Assert.equal(lastFrame.depth, 1);
+ Assert.equal(lastFrame.formatedDisplayName, "λ bar");
+ Assert.equal(lastFrame.prefix, "testContentPrefix: ");
+ Assert.ok(lastFrame.frame);
+
+ info("Stop tracing");
+ stopTracing();
+ Assert.equal(toggles.length, 2);
+ Assert.equal(toggles[1], false);
+
+ info("Recall code after stop, no more traces are logged");
+ sandbox.foo();
+ Assert.equal(frames.length, 0);
+
+ info("Start tracing again, and recall code");
+ startTracing({ global: sandbox, prefix: "testContentPrefix" });
+ sandbox.foo();
+ info("New traces are logged");
+ Assert.equal(frames.length, 2);
+
+ info("Unregister the listener and recall code");
+ removeTracingListener(listener);
+ sandbox.foo();
+ info("No more traces are logged");
+ Assert.equal(frames.length, 2);
+
+ info("Stop tracing");
+ stopTracing();
+});
+
+add_task(async function testTracingJSMGlobal() {
+ // We have to register the listener code in a sandbox, i.e. in a distinct global
+ // so that we aren't creating traces when tracer calls it. (and cause infinite loops)
+ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ const listenerSandbox = Cu.Sandbox(systemPrincipal);
+ Cu.evalInSandbox(
+ "new " +
+ function () {
+ globalThis.toggles = [];
+ globalThis.frames = [];
+ globalThis.listener = {
+ onTracingToggled(state) {
+ globalThis.toggles.push(state);
+ },
+ onTracingFrame(frameInfo) {
+ globalThis.frames.push(frameInfo);
+ },
+ };
+ },
+ listenerSandbox
+ );
+
+ info("Register a tracing listener");
+ addTracingListener(listenerSandbox.listener);
+
+ info("Start tracing");
+ startTracing({ global: null, prefix: "testPrefix" });
+ Assert.equal(listenerSandbox.toggles.length, 1);
+ Assert.equal(listenerSandbox.toggles[0], true);
+
+ info("Call some code");
+ function bar() {}
+ function foo() {
+ bar();
+ }
+ foo();
+
+ // Note that the tracer will record the two Assert.equal and the info calls.
+ // So only assert the last two frames.
+ const lastFrame = listenerSandbox.frames.at(-1);
+ const beforeLastFrame = listenerSandbox.frames.at(-2);
+ Assert.equal(beforeLastFrame.depth, 0);
+ Assert.equal(beforeLastFrame.formatedDisplayName, "λ foo");
+ Assert.equal(beforeLastFrame.prefix, "testPrefix: ");
+ Assert.ok(beforeLastFrame.frame);
+ Assert.equal(lastFrame.depth, 1);
+ Assert.equal(lastFrame.formatedDisplayName, "λ bar");
+ Assert.equal(lastFrame.prefix, "testPrefix: ");
+ Assert.ok(lastFrame.frame);
+
+ info("Stop tracing");
+ stopTracing();
+ Assert.equal(listenerSandbox.toggles.length, 2);
+ Assert.equal(listenerSandbox.toggles[1], false);
+
+ removeTracingListener(listenerSandbox.listener);
+});
diff --git a/devtools/server/tracer/tests/xpcshell/xpcshell.ini b/devtools/server/tracer/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..4864934b9e
--- /dev/null
+++ b/devtools/server/tracer/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = devtools
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_tracer.js]
diff --git a/devtools/server/tracer/tracer.jsm b/devtools/server/tracer/tracer.jsm
new file mode 100644
index 0000000000..44bf83104d
--- /dev/null
+++ b/devtools/server/tracer/tracer.jsm
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module implements the JavaScript tracer.
+ *
+ * It is being used by:
+ * - any code that want to manually toggle the tracer, typically when debugging code,
+ * - the tracer actor to start and stop tracing from DevTools UI,
+ * - the tracing state resource watcher in order to notify DevTools UI about the tracing state.
+ *
+ * It will default logging the tracers to the terminal/stdout.
+ * But if DevTools are opened, it may delegate the logging to the tracer actor.
+ * It will typically log the traces to the Web Console.
+ *
+ * `JavaScriptTracer.onEnterFrame` method is hot codepath and should be reviewed accordingly.
+ */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = [
+ "startTracing",
+ "stopTracing",
+ "addTracingListener",
+ "removeTracingListener",
+];
+
+const listeners = new Set();
+
+// 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.
+const customLazy = {
+ get Debugger() {
+ // When this code runs in the worker thread, this module runs within
+ // the WorkerDebuggerGlobalScope and have immediate access to Debugger class.
+ // (while we can't use ChromeUtils.importESModule)
+ if (globalThis.Debugger) {
+ return globalThis.Debugger;
+ }
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ // Avoid polluting all Modules global scope by using a Sandox as global.
+ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ const debuggerSandbox = Cu.Sandbox(systemPrincipal);
+ addDebuggerToGlobal(debuggerSandbox);
+ delete customLazy.Debugger;
+ customLazy.Debugger = debuggerSandbox.Debugger;
+ return customLazy.Debugger;
+ },
+
+ get DistinctCompartmentDebugger() {
+ const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+ );
+ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ const debuggerSandbox = Cu.Sandbox(systemPrincipal, {
+ // As we may debug the JSM/ESM shared global, we should be using a Debugger
+ // from another system global.
+ freshCompartment: true,
+ });
+ addDebuggerToGlobal(debuggerSandbox);
+ delete customLazy.DistinctCompartmentDebugger;
+ customLazy.DistinctCompartmentDebugger = debuggerSandbox.Debugger;
+ return customLazy.DistinctCompartmentDebugger;
+ },
+};
+
+/**
+ * Start tracing against a given JS global.
+ * Only code run from that global will be logged.
+ *
+ * @param {Object} options
+ * Object with configurations:
+ * @param {Object} options.global
+ * The tracer only log traces related to the code executed within this global.
+ * When omitted, it will default to the options object's global.
+ * @param {String} options.prefix
+ * Optional string logged as a prefix to all traces.
+ * @param {Debugger} options.dbg
+ * Optional spidermonkey's Debugger instance.
+ * This allows devtools to pass a custom instance and ease worker support
+ * where we can't load jsdebugger.sys.mjs.
+ */
+class JavaScriptTracer {
+ constructor(options) {
+ this.onEnterFrame = this.onEnterFrame.bind(this);
+
+ // 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.
+ const global = options.global || Cu.getGlobalForObject(options);
+
+ // Instantiate a brand new Debugger API so that we can trace independently
+ // of all other DevTools operations. i.e. we can pause while tracing without any interference.
+ this.dbg = this.makeDebugger(global);
+
+ this.depth = 0;
+ this.prefix = options.prefix ? `${options.prefix}: ` : "";
+
+ this.dbg.onEnterFrame = this.onEnterFrame;
+
+ this.notifyToggle(true);
+ }
+
+ stopTracing() {
+ if (!this.isTracing()) {
+ return;
+ }
+
+ this.dbg.onEnterFrame = undefined;
+ this.dbg.removeAllDebuggees();
+ this.dbg.onNewGlobalObject = undefined;
+ this.dbg = null;
+ this.depth = 0;
+ this.options = null;
+
+ this.notifyToggle(false);
+ }
+
+ isTracing() {
+ return !!this.dbg;
+ }
+
+ /**
+ * Instantiate a Debugger API instance dedicated to each Tracer instance.
+ * It will notably be different from the instance used in DevTools.
+ * This allows to implement tracing independently of DevTools.
+ *
+ * @param {Object} global
+ * The global to trace.
+ */
+ makeDebugger(global) {
+ // When this code runs in the worker thread, Cu isn't available
+ // and we don't have system principal anyway in this context.
+ const { isSystemPrincipal } =
+ typeof Cu == "object" ? Cu.getObjectPrincipal(global) : {};
+
+ // When debugging the system modules, we have to use a special instance
+ // of Debugger loaded in a distinct system global.
+ const dbg = isSystemPrincipal
+ ? new customLazy.DistinctCompartmentDebugger()
+ : new customLazy.Debugger();
+
+ // By default, only track the global passed as argument
+ dbg.addDebuggee(global);
+
+ return dbg;
+ }
+
+ /**
+ * Notify DevTools and/or the user via stdout that tracing
+ * has been enabled or disabled.
+ *
+ * @param {Boolean} state
+ * True if we just started tracing, false when it just stopped.
+ */
+ notifyToggle(state) {
+ let shouldLogToStdout = listeners.size == 0;
+ for (const listener of listeners) {
+ if (typeof listener.onTracingToggled == "function") {
+ shouldLogToStdout |= listener.onTracingToggled(state);
+ }
+ }
+ if (shouldLogToStdout) {
+ if (state) {
+ dump(this.prefix + "Start tracing JavaScript\n");
+ } else {
+ dump(this.prefix + "Stop tracing JavaScript\n");
+ }
+ }
+ }
+
+ /**
+ * Notify DevTools and/or the user via stdout that tracing
+ * stopped because of an infinite loop.
+ */
+ notifyInfiniteLoop() {
+ let shouldLogToStdout = listeners.size == 0;
+ for (const listener of listeners) {
+ if (typeof listener.onTracingInfiniteLoop == "function") {
+ shouldLogToStdout |= listener.onTracingInfiniteLoop();
+ }
+ }
+ if (shouldLogToStdout) {
+ dump(
+ this.prefix +
+ "Looks like an infinite recursion? We stopped the JavaScript tracer, but code may still be running!\n"
+ );
+ }
+ }
+
+ /**
+ * Called by the Debugger API (this.dbg) when a new frame is executed.
+ *
+ * @param {Debugger.Frame} frame
+ * A descriptor object for the JavaScript frame.
+ */
+ onEnterFrame(frame) {
+ // Safe check, just in case we keep being notified, but the tracer has been stopped
+ if (!this.dbg) {
+ return;
+ }
+ try {
+ if (this.depth == 100) {
+ this.notifyInfiniteLoop();
+ this.stopTracing();
+ return;
+ }
+
+ const formatedDisplayName = formatDisplayName(frame);
+ let shouldLogToStdout = true;
+
+ // If there is at least one DevTools debugging this process,
+ // delegate logging to DevTools actors.
+ if (listeners.size > 0) {
+ shouldLogToStdout = false;
+ for (const listener of listeners) {
+ // If any listener return true, also log to stdout
+ if (typeof listener.onTracingFrame == "function") {
+ shouldLogToStdout |= listener.onTracingFrame({
+ frame,
+ depth: this.depth,
+ formatedDisplayName,
+ prefix: this.prefix,
+ });
+ }
+ }
+ }
+
+ // DevTools may delegate the work to log to stdout,
+ // but if DevTools are closed, stdout is the only way to log the traces.
+ if (shouldLogToStdout) {
+ const { script } = frame;
+ const { lineNumber, columnNumber } = script.getOffsetMetadata(
+ frame.offset
+ );
+ const padding = "—".repeat(this.depth + 1);
+ // Use a special URL, including line and column numbers which Firefox
+ // interprets as to be opened in the already opened DevTool's debugger
+ const href = `${script.source.url}:${lineNumber}:${columnNumber}`;
+
+ // Use special characters in order to print working hyperlinks right from the terminal
+ // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+ const urlLink = `\x1B]8;;${href}\x1B\\${href}\x1B]8;;\x1B\\`;
+
+ const message = `${padding}[${
+ frame.implementation
+ }]—> ${urlLink} - ${formatDisplayName(frame)}`;
+
+ dump(this.prefix + message + "\n");
+ }
+
+ this.depth++;
+ frame.onPop = () => {
+ this.depth--;
+ };
+ } catch (e) {
+ console.error("Exception while tracing javascript", e);
+ }
+ }
+}
+
+/**
+ * Try to describe the current frame we are tracing
+ *
+ * This will typically log the name of the method being called.
+ *
+ * @param {Debugger.Frame} frame
+ * The frame which is currently being executed.
+ */
+function formatDisplayName(frame) {
+ if (frame.type === "call") {
+ const callee = frame.callee;
+ // Anonymous function will have undefined name and displayName.
+ return "λ " + (callee.name || callee.displayName || "anonymous");
+ }
+
+ return `(${frame.type})`;
+}
+
+let activeTracer = null;
+
+/**
+ * Start tracing JavaScript.
+ * i.e. log the name of any function being called in JS and its location in source code.
+ *
+ * @params {Object} options (mandatory)
+ * See JavaScriptTracer.startTracing jsdoc.
+ */
+function startTracing(options) {
+ if (!options) {
+ throw new Error("startTracing excepts an options object as first argument");
+ }
+ if (!activeTracer) {
+ activeTracer = new JavaScriptTracer(options);
+ } else {
+ console.warn(
+ "Can't start JavaScript tracing, another tracer is still active and we only support one tracer at a time."
+ );
+ }
+}
+
+/**
+ * Stop tracing JavaScript.
+ */
+function stopTracing() {
+ if (activeTracer) {
+ activeTracer.stopTracing();
+ activeTracer = null;
+ } else {
+ console.warn("Can't stop JavaScript Tracing as we were not tracing.");
+ }
+}
+
+/**
+ * Listen for tracing updates.
+ *
+ * The listener object may expose the following methods:
+ * - onTracingToggled(state)
+ * Where state is a boolean to indicate if tracing has just been enabled of disabled.
+ * It may be immediatelly called if a tracer is already active.
+ *
+ * - onTracingInfiniteLoop()
+ * Called when the tracer stopped because of an infinite loop.
+ *
+ * - onTracingFrame({ frame, depth, formatedDisplayName, prefix })
+ * Called each time we enter a new JS frame.
+ * - frame is a Debugger.Frame object
+ * - depth is a number and represents the depth of the frame in the call stack
+ * - formatedDisplayName is a string and is a human readable name for the current frame
+ * - prefix is a string to display as a prefix of any logged frame
+ *
+ * @param {Object} listener
+ */
+function addTracingListener(listener) {
+ listeners.add(listener);
+
+ if (
+ activeTracer?.isTracing() &&
+ typeof listener.onTracingToggled == "function"
+ ) {
+ listener.onTracingToggled(true);
+ }
+}
+
+/**
+ * Unregister a listener previous registered via addTracingListener
+ */
+function removeTracingListener(listener) {
+ listeners.delete(listener);
+}
+
+// This JSM may be execute as CommonJS when loaded in the worker thread
+if (typeof module == "object") {
+ module.exports = {
+ startTracing,
+ stopTracing,
+ addTracingListener,
+ removeTracingListener,
+ };
+}