Bug 1848179 - [devtools] Display the currently processed DOM event for top frames. r=devtools-reviewers,nchevobbe

As this is based on DebuggerNotificationObserver, this would only work
for events currently supported by this interface.
For example, we would miss network events which aren't suppported yet.

I kept this as an option for the base tracer class in order to be able to have a fine control
on its output, but kept things simple in the Debugger UI and made this be enabled by default.

Differential Revision: https://phabricator.services.mozilla.com/D185934
This commit is contained in:
Alexandre Poirot 2023-10-25 09:38:03 +00:00
parent c7283bdb04
commit b37240ecdb
5 changed files with 188 additions and 10 deletions

View File

@ -43,6 +43,16 @@ add_task(async function () {
await waitForSelectedSource(dbg, "simple1.js");
await waitForSelectedLocation(dbg, 1, 16);
// Trigger a click to verify we do trace DOM events
BrowserTestUtils.synthesizeMouseAtCenter(
"button",
{},
gBrowser.selectedBrowser
);
await hasConsoleMessage(dbg, "DOM(click)");
await hasConsoleMessage(dbg, "λ simple");
// Test Blackboxing
info("Clear the console from previous traces");
const { hud } = await dbg.toolbox.getPanel("webconsole");
@ -111,6 +121,7 @@ add_task(async function () {
await hasConsoleMessage(dbg, "λ logMessage");
// Test clicking on the function to open the precise related location
const traceMessages2 = await findConsoleMessages(dbg.toolbox, "λ logMessage");
is(
traceMessages2.length,

View File

@ -36,6 +36,11 @@ const CONSOLE_ARGS_STYLES = [
"color: var(--theme-highlight-blue); margin-inline: 2px;",
];
const DOM_EVENT_CONSOLE_ARGS_STYLES = [
"color: var(--theme-toolbarbutton-checked-hover-background)",
"padding-inline: 4px; margin-inline: 2px; background-color: var(--toolbarbutton-checked-background); color: var(--toolbarbutton-checked-color);",
];
const CONSOLE_THROTTLING_DELAY = 250;
class TracerActor extends Actor {
@ -73,6 +78,8 @@ class TracerActor extends Actor {
addTracingListener(this.tracingListener);
startTracing({
global: this.targetActor.window || this.targetActor.workerGlobal,
// Enable receiving the `currentDOMEvent` being passed to `onTracingFrame`
traceDOMEvents: true,
});
}
@ -124,10 +131,20 @@ class TracerActor extends Actor {
* A human readable name for the current frame.
* @param {String} prefix
* A string to be displayed as a prefix of any logged frame.
* @param {String} currentDOMEvent
* If this is a top level frame (depth==0), and we are currently processing
* a DOM Event, this will refer to the name of that DOM Event.
* Note that it may also refer to setTimeout and setTimeout callback calls.
* @return {Boolean}
* Return true, if the JavaScriptTracer should log the frame to stdout.
*/
onTracingFrame({ frame, depth, formatedDisplayName, prefix }) {
onTracingFrame({
frame,
depth,
formatedDisplayName,
prefix,
currentDOMEvent,
}) {
const { script } = frame;
const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
const url = script.source.url;
@ -142,6 +159,21 @@ class TracerActor extends Actor {
return true;
}
// We may receive the currently processed DOM event (if this relates to one).
// In this case, log a preliminary message, which looks different to highlight it.
if (currentDOMEvent && depth == 0) {
const DOMEventArgs = [prefix + "—", currentDOMEvent];
// Create a message object that fits Console Message Watcher expectations
this.throttledConsoleMessages.push({
arguments: DOMEventArgs,
styles: DOM_EVENT_CONSOLE_ARGS_STYLES,
level: "logTrace",
chromeContext: this.isChromeContext,
timeStamp: ChromeUtils.dateNow(),
});
}
const args = [
prefix + "—".repeat(depth + 1),
frame.implementation,

View File

@ -7,3 +7,5 @@ support-files = [
"Worker.tracer.js",
"WorkerDebugger.tracer.js",
]
["browser_document_tracer.js"]

View File

@ -0,0 +1,68 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const JS_CODE = `
window.onclick = function foo() {
setTimeout(function bar() {
dump("click and timed out\n");
});
};
`;
const TEST_URL =
"data:text/html,<!DOCTYPE html><html><script>" + JS_CODE + " </script>";
add_task(async function testTracingWorker() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
const {
addTracingListener,
removeTracingListener,
startTracing,
stopTracing,
} = ChromeUtils.import("resource://devtools/server/tracer/tracer.jsm");
// We have to fake opening DevTools otherwise DebuggerNotificationObserver wouldn't work
// and the tracer wouldn't be able to trace the DOM events.
ChromeUtils.notifyDevToolsOpened();
const frames = [];
const listener = {
onTracingFrame(frameInfo) {
frames.push(frameInfo);
},
};
info("Register a tracing listener");
addTracingListener(listener);
info("Start tracing the iframe");
startTracing({ global: content, traceDOMEvents: true });
info("Dispatch a click event on the iframe");
EventUtils.synthesizeMouseAtCenter(
content.document.documentElement,
{},
content
);
info("Wait for the traces generated by this click");
await ContentTaskUtils.waitForCondition(() => frames.length == 2);
const firstFrame = frames[0];
is(firstFrame.formatedDisplayName, "λ foo");
is(firstFrame.currentDOMEvent, "DOM(click)");
const lastFrame = frames.at(-1);
is(lastFrame.formatedDisplayName, "λ bar");
is(lastFrame.currentDOMEvent, "setTimeoutCallback");
stopTracing();
removeTracingListener(listener);
ChromeUtils.notifyDevToolsClosed();
});
BrowserTestUtils.removeTab(tab);
});

View File

@ -83,6 +83,9 @@ const customLazy = {
* 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.
* @param {Boolean} options.traceDOMEvents
* Optional setting to enable tracing all the DOM events being going through
* dom/events/EventListenerManager.cpp's `EventListenerManager`.
*/
class JavaScriptTracer {
constructor(options) {
@ -91,20 +94,69 @@ class JavaScriptTracer {
// 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);
this.tracedGlobal = 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.dbg = this.makeDebugger();
this.depth = 0;
this.prefix = options.prefix ? `${options.prefix}: ` : "";
this.dbg.onEnterFrame = this.onEnterFrame;
this.traceDOMEvents = !!options.traceDOMEvents;
if (this.traceDOMEvents) {
this.startTracingDOMEvents();
}
this.notifyToggle(true);
}
startTracingDOMEvents() {
this.debuggerNotificationObserver = new DebuggerNotificationObserver();
this.eventListener = this.eventListener.bind(this);
this.debuggerNotificationObserver.addListener(this.eventListener);
this.debuggerNotificationObserver.connect(this.tracedGlobal);
this.currentDOMEvent = null;
}
stopTracingDOMEvents() {
this.debuggerNotificationObserver.removeListener(this.eventListener);
this.debuggerNotificationObserver.disconnect(this.tracedGlobal);
this.debuggerNotificationObserver = null;
this.currentDOMEvent = null;
}
/**
* Called by DebuggerNotificationObserver interface when a DOM event start being notified
* and after it has been notified.
*
* @param {DebuggerNotification} notification
* Info about the DOM event. See the related idl file.
*/
eventListener(notification) {
// For each event we get two notifications.
// One just before firing the listeners and another one just after.
//
// Update `this.currentDOMEvent` to be refering to the event name
// while the DOM event is being notified. It will be null the rest of the time.
//
// We don't need to maintain a stack of events as that's only consumed by onEnterFrame
// which only cares about the very lastest event being currently trigerring some code.
if (notification.phase == "pre") {
// We get notified about "real" DOM event, but also when some particular callbacks are called like setTimeout.
if (notification.type == "domEvent") {
this.currentDOMEvent = `DOM(${notification.event.type})`;
} else {
this.currentDOMEvent = notification.type;
}
} else {
this.currentDOMEvent = null;
}
}
stopTracing() {
if (!this.isTracing()) {
return;
@ -117,6 +169,12 @@ class JavaScriptTracer {
this.depth = 0;
this.options = null;
if (this.traceDOMEvents) {
this.stopTracingDOMEvents();
}
this.tracedGlobal = null;
this.notifyToggle(false);
}
@ -128,15 +186,12 @@ class JavaScriptTracer {
* 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) {
makeDebugger() {
// 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) : {};
typeof Cu == "object" ? Cu.getObjectPrincipal(this.tracedGlobal) : {};
// When debugging the system modules, we have to use a special instance
// of Debugger loaded in a distinct system global.
@ -144,8 +199,9 @@ class JavaScriptTracer {
? new customLazy.DistinctCompartmentDebugger()
: new customLazy.Debugger();
// By default, only track the global passed as argument
dbg.addDebuggee(global);
// For now, we only trace calls for one particular global at a time.
// See the constructor for its definition.
dbg.addDebuggee(this.tracedGlobal);
return dbg;
}
@ -225,6 +281,7 @@ class JavaScriptTracer {
depth: this.depth,
formatedDisplayName,
prefix: this.prefix,
currentDOMEvent: this.currentDOMEvent,
});
}
}
@ -238,6 +295,14 @@ class JavaScriptTracer {
frame.offset
);
const padding = "—".repeat(this.depth + 1);
// If we are tracing DOM events and we are in middle of an event,
// and are logging the topmost frame,
// then log a preliminary dedicated line to mention that event type.
if (this.currentDOMEvent && this.depth == 0) {
dump(this.prefix + padding + this.currentDOMEvent + "\n");
}
// 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}`;