mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
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:
parent
c7283bdb04
commit
b37240ecdb
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -7,3 +7,5 @@ support-files = [
|
||||
"Worker.tracer.js",
|
||||
"WorkerDebugger.tracer.js",
|
||||
]
|
||||
|
||||
["browser_document_tracer.js"]
|
||||
|
@ -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);
|
||||
});
|
@ -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}`;
|
||||
|
Loading…
Reference in New Issue
Block a user