Bug 1848159 - [devtools] Use a SessionData/Target configuration to control JavaScript tracer. r=devtools-reviewers,nchevobbe

This simplifies toggling the Tracer on all active targets.
But this will be especially useful in the next changeset.
This allows to have two distinct level of enabling:
* the target configuration
* the actual start of tracing done by the tracer (on user interaction or next page load)

Doing this allows to distinguish tracer simple enabling,
when "trace on next user interaction" is enabled,
where we can display a badge on the tracer icon to significate it isn't tracing just yet.
And actual start of the tracer, when the first user interaction happens,
where we can remove the badge.

Differential Revision: https://phabricator.services.mozilla.com/D198961
This commit is contained in:
Alexandre Poirot 2024-01-30 20:18:50 +00:00
Родитель 5cb9ec4cb0
Коммит 291dec5ed3
13 изменённых файлов: 259 добавлений и 84 удалений

Просмотреть файл

@ -2,7 +2,7 @@
* 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/>. */
import { getIsThreadCurrentlyTracing, getAllThreads } from "../selectors/index";
import { getIsJavascriptTracingEnabled } from "../selectors/index";
import { PROMISE } from "./utils/middleware/promise";
/**
@ -14,12 +14,8 @@ import { PROMISE } from "./utils/middleware/promise";
*/
export function toggleTracing(logMethod) {
return async ({ dispatch, getState, client, panel }) => {
// Check if any of the thread is currently tracing.
// For now, the UI can only toggle all the targets all at once.
const threads = getAllThreads(getState());
const isTracingEnabled = threads.some(thread =>
getIsThreadCurrentlyTracing(getState(), thread.actor)
);
const isTracingEnabled = getIsJavascriptTracingEnabled(getState());
// Automatically open the split console when enabling tracing to the console
if (!isTracingEnabled && logMethod == "console") {
@ -29,6 +25,7 @@ export function toggleTracing(logMethod) {
return dispatch({
type: "TOGGLE_TRACING",
[PROMISE]: client.toggleTracing(logMethod),
enabled: !isTracingEnabled,
});
};
}

Просмотреть файл

@ -14,6 +14,7 @@ import {
getCurrentThread,
isTopFrameSelected,
getIsCurrentThreadPaused,
getIsJavascriptTracingEnabled,
getIsThreadCurrentlyTracing,
getJavascriptTracingLogMethod,
getJavascriptTracingValues,
@ -203,13 +204,24 @@ class CommandBar extends Component {
if (!features.javascriptTracing) {
return null;
}
// The button is highlighted in blue as soon as the user requested to start the trace
const isActive = this.props.isTracingEnabled;
// But it will only be active once the tracer actually started.
// This may come later when using "on next user interaction" feature.
const isPending = isActive && !this.props.isTracingActive;
let className = "";
if (isPending) {
className = "pending";
} else if (isActive) {
className = "active";
}
// Display a button which:
// - on left click, would toggle on/off javascript tracing
// - on right click, would display a context menu allowing to choose the logging output (console or stdout)
// - on right click, would display a context menu to configure the tracer settings
return button({
className: `devtools-button command-bar-button debugger-trace-menu-button ${
this.props.isTracingEnabled ? "active" : ""
}`,
className: `devtools-button command-bar-button debugger-trace-menu-button ${className}`,
title: this.props.isTracingEnabled
? L10N.getFormatStr("stopTraceButtonTooltip2", formatKey("trace"))
: L10N.getFormatStr(
@ -432,7 +444,11 @@ const mapStateToProps = state => ({
topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
javascriptEnabled: state.ui.javascriptEnabled,
isPaused: getIsCurrentThreadPaused(state),
isTracingEnabled: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
isTracingEnabled: getIsJavascriptTracingEnabled(
state,
getCurrentThread(state)
),
isTracingActive: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
logMethod: getJavascriptTracingLogMethod(state),
logValues: getJavascriptTracingValues(state),
traceOnNextInteraction: getJavascriptTracingOnNextInteraction(state),

Просмотреть файл

@ -58,6 +58,16 @@
.devtools-button.debugger-trace-menu-button::before {
background-image: url(chrome://devtools/content/debugger/images/trace.svg);
}
.devtools-button.debugger-trace-menu-button.active::before {
.devtools-button.debugger-trace-menu-button:is(.active, .pending)::before {
fill: var(--theme-icon-checked-color);
}
.devtools-button.debugger-trace-menu-button.pending::after
{
content: url("chrome://global/skin/icons/badge-blue.svg");
width: 14px;
height: 14px;
display: block;
position: absolute;
bottom: -2px;
right: 0;
}

Просмотреть файл

@ -2,7 +2,7 @@
* 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/>. */
/* eslint complexity: ["error", 35]*/
/* eslint-disable complexity */
/**
* UI reducer
@ -35,6 +35,7 @@ export const initialUIState = () => ({
inlinePreviewEnabled: features.inlinePreview,
editorWrappingEnabled: prefs.editorWrapping,
javascriptEnabled: true,
javascriptTracingEnabled: false,
javascriptTracingLogMethod: prefs.javascriptTracingLogMethod,
javascriptTracingValues: prefs.javascriptTracingValues,
javascriptTracingOnNextInteraction: prefs.javascriptTracingOnNextInteraction,
@ -159,6 +160,13 @@ function update(state = initialUIState(), action) {
return state;
}
case "TOGGLE_TRACING": {
if (action.status === "start") {
return { ...state, javascriptTracingEnabled: action.enabled };
}
return state;
}
case "SET_JAVASCRIPT_TRACING_LOG_METHOD": {
prefs.javascriptTracingLogMethod = action.value;
return { ...state, javascriptTracingLogMethod: action.value };

Просмотреть файл

@ -3,6 +3,7 @@
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { getSelectedSource } from "./sources";
import { getIsThreadCurrentlyTracing, getAllThreads } from "./threads";
export function getSelectedPrimaryPaneTab(state) {
return state.ui.selectedPrimaryPaneTab;
@ -68,6 +69,17 @@ export function getEditorWrapping(state) {
return state.ui.editorWrappingEnabled;
}
export function getIsJavascriptTracingEnabled(state) {
// Check for the global state which may be set by debugger toggling,
// but also on individual thread state which will be set by the `:trace` console command.
return (
state.ui.javascriptTracingEnabled ||
getAllThreads(state).some(thread =>
getIsThreadCurrentlyTracing(state, thread.actor)
)
);
}
export function getJavascriptTracingLogMethod(state) {
return state.ui.javascriptTracingLogMethod;
}

Просмотреть файл

@ -438,12 +438,15 @@ add_task(async function testTracingOnNextInteraction() {
await clickElement(dbg, "trace");
const topLevelThreadActorID =
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
info("Wait for tracing to be enabled");
await waitForState(dbg, state => {
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
const traceButton = findElement(dbg, "trace");
// Wait for the trace button to be highlighted
await waitFor(() => {
return traceButton.classList.contains("pending");
});
ok(
traceButton.classList.contains("pending"),
"The tracer button is also highlighted as pending until the user interaction is triggered"
);
invokeInTab("foo");
@ -469,9 +472,25 @@ add_task(async function testTracingOnNextInteraction() {
);
AccessibilityUtils.resetEnv();
let topLevelThreadActorID =
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
info("Wait for tracing to be enabled");
await waitForState(dbg, state => {
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
});
await hasConsoleMessage(dbg, "λ onmousedown");
await hasConsoleMessage(dbg, "λ onclick");
ok(
traceButton.classList.contains("active"),
"The tracer button is still highlighted as active"
);
ok(
!traceButton.classList.contains("pending"),
"The tracer button is no longer pending after the user interaction"
);
is(
(await findConsoleMessages(dbg.toolbox, "λ foo")).length,
0,
@ -483,6 +502,24 @@ add_task(async function testTracingOnNextInteraction() {
await hasConsoleMessage(dbg, "λ foo");
ok(true, "foo was traced as expected");
await clickElement(dbg, "trace");
topLevelThreadActorID =
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
info("Wait for tracing to be disabled");
await waitForState(dbg, state => {
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
});
ok(
!traceButton.classList.contains("active"),
"The tracer button is no longer highlighted as active"
);
ok(
!traceButton.classList.contains("pending"),
"The tracer button is still not pending after disabling"
);
// Reset the trace on next interaction setting
Services.prefs.clearUserPref(
"devtools.debugger.javascript-tracing-on-next-interaction"

Просмотреть файл

@ -47,7 +47,10 @@ add_task(async function testBasicRecord() {
":trace --logMethod console --prefix foo --values --on-next-interaction",
"console-api"
);
is(msg.textContent.trim(), "Started tracing to Web Console");
is(
msg.textContent.trim(),
"Waiting for next user interaction before tracing (next mousedown or keydown event)"
);
info("Trigger some code before the user interaction");
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
@ -62,6 +65,11 @@ add_task(async function testBasicRecord() {
content.wrappedJSObject.main("arg", 2);
});
info("Ensure a message notified about the tracer actual start");
await waitFor(
() => !!findConsoleAPIMessage(hud, `Started tracing to Web Console`)
);
// Assert that we also see the custom prefix, as well as function arguments
await waitFor(
() => !!findTracerMessages(hud, `foo: interpreter⟶λ main("arg", 2)`).length

Просмотреть файл

@ -51,6 +51,8 @@ const SUPPORTED_OPTIONS = {
setTabOffline: true,
// Enable touch events simulation
touchEventsOverride: true,
// Used to configure and start/stop the JavaScript tracer
tracerOptions: true,
// Use simplified highlighters when prefers-reduced-motion is enabled.
useSimpleHighlightersForReducedMotion: true,
};

Просмотреть файл

@ -107,6 +107,9 @@ class BaseTargetActor extends Actor {
);
}
// List of actor prefixes (string) which have already been instantiated via getTargetScopedActor method.
#instantiatedTargetScopedActors = new Set();
/**
* Try to return any target scoped actor instance, if it exists.
* They are lazily instantiated and so will only be available
@ -121,7 +124,38 @@ class BaseTargetActor extends Actor {
return null;
}
const form = this.form();
this.#instantiatedTargetScopedActors.add(prefix);
return this.conn._getOrCreateActor(form[prefix + "Actor"]);
}
/**
* Returns true, if the related target scoped actor has already been queried
* and instantiated via `getTargetScopedActor` method.
*
* @param {String} prefix
* See getTargetScopedActor definition
* @return Boolean
* True, if the actor has already been instantiated.
*/
hasTargetScopedActor(prefix) {
return this.#instantiatedTargetScopedActors.has(prefix);
}
/**
* Apply target-specific options.
*
* This will be called by the watcher when the DevTools target-configuration
* is updated, or when a target is created via JSWindowActors.
*/
updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) {
// If there is some tracer options, we should start tracing, otherwise we should stop (if we were)
if (options.tracerOptions) {
const tracerActor = this.getTargetScopedActor("tracer");
tracerActor.startTracing(options.tracerOptions);
} else if (this.hasTargetScopedActor("tracer")) {
const tracerActor = this.getTargetScopedActor("tracer");
tracerActor.stopTracing();
}
}
}
exports.BaseTargetActor = BaseTargetActor;

Просмотреть файл

@ -1308,6 +1308,9 @@ class WindowGlobalTargetActor extends BaseTargetActor {
return;
}
// Also update configurations which applies to all target types
super.updateTargetConfiguration(options, calledFromDocumentCreation);
let reload = false;
if (typeof options.touchEventsOverride !== "undefined") {
const enableTouchSimulator = options.touchEventsOverride === "enabled";

Просмотреть файл

@ -12,6 +12,7 @@ const {
stopTracing,
addTracingListener,
removeTracingListener,
NEXT_INTERACTION_MESSAGE,
} = require("resource://devtools/server/tracer/tracer.jsm");
const { Actor } = require("resource://devtools/shared/protocol.js");
@ -83,7 +84,7 @@ class TracerActor extends Actor {
*/
toggleTracing(options) {
if (!this.tracingListener) {
this.#startTracing(options);
this.startTracing(options);
return true;
}
this.stopTracing();
@ -93,18 +94,11 @@ class TracerActor extends Actor {
/**
* Start tracing.
*
* @param {String} logMethod
* The output method used by the tracer.
* See `LOG_METHODS` for potential values.
* @param {Object} options
* Options used to configure JavaScriptTracer.
* See `JavaScriptTracer.startTracing`.
*/
startTracing(logMethod = LOG_METHODS.STDOUT, options = {}) {
this.#startTracing({ ...options, logMethod });
}
#startTracing(options) {
startTracing(options = {}) {
if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) {
throw new Error(
`Invalid log method '${options.logMethod}'. Only supports: ${VALID_LOG_METHODS}`
@ -119,6 +113,14 @@ class TracerActor extends Actor {
if (options.maxRecords && typeof options.maxRecords != "number") {
throw new Error("Invalid max-records, only support numbers");
}
// When tracing on next user interaction is enabled,
// disable logging from workers as this makes the tracer work
// against visible documents and is actived per document thread.
if (options.traceOnNextInteraction && isWorker) {
return;
}
this.logMethod = options.logMethod || LOG_METHODS.STDOUT;
if (this.logMethod == LOG_METHODS.PROFILER) {
@ -129,6 +131,7 @@ class TracerActor extends Actor {
onTracingFrame: this.onTracingFrame.bind(this),
onTracingInfiniteLoop: this.onTracingInfiniteLoop.bind(this),
onTracingToggled: this.onTracingToggled.bind(this),
onTracingPending: this.onTracingPending.bind(this),
};
addTracingListener(this.tracingListener);
this.traceValues = !!options.traceValues;
@ -152,10 +155,12 @@ class TracerActor extends Actor {
if (!this.tracingListener) {
return;
}
stopTracing();
// Remove before stopping to prevent receiving the stop notification
removeTracingListener(this.tracingListener);
this.logMethod = null;
this.tracingListener = null;
stopTracing();
this.logMethod = null;
}
/**
@ -195,6 +200,37 @@ class TracerActor extends Actor {
return shouldLogToStdout;
}
/**
* Called when "trace on next user interaction" is enabled, to notify the user
* that the tracer is initialized but waiting for the user first input.
*/
onTracingPending() {
// Delegate to JavaScriptTracer to log to stdout
if (this.logMethod == LOG_METHODS.STDOUT) {
return true;
}
if (this.logMethod == LOG_METHODS.CONSOLE) {
const consoleMessageWatcher = getResourceWatcher(
this.targetActor,
TYPES.CONSOLE_MESSAGE
);
if (consoleMessageWatcher) {
consoleMessageWatcher.emitMessages([
{
arguments: [NEXT_INTERACTION_MESSAGE],
styles: [],
level: "log",
chromeContext: false,
timeStamp: ChromeUtils.dateNow(),
},
]);
}
return false;
}
return false;
}
onTracingInfiniteLoop() {
if (this.logMethod == LOG_METHODS.STDOUT) {
return true;

Просмотреть файл

@ -24,8 +24,12 @@ const EXPORTED_SYMBOLS = [
"stopTracing",
"addTracingListener",
"removeTracingListener",
"NEXT_INTERACTION_MESSAGE",
];
const NEXT_INTERACTION_MESSAGE =
"Waiting for next user interaction before tracing (next mousedown or keydown event)";
const listeners = new Set();
// This module can be loaded from the worker thread, where we can't use ChromeUtils.
@ -163,20 +167,43 @@ class JavaScriptTracer {
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");
}
} else {
this.#startTracing();
}
// In any case, we consider the tracing as started
this.notifyToggle(true);
}
// Is actively tracing?
// We typically start tracing from the constructor, unless the "trace on next user interaction" feature is used.
isTracing = false;
/**
* Actually really start watching for executions.
*
* This may be delayed when traceOnNextInteraction options is used.
* Otherwise we start tracing as soon as the class instantiates.
*/
#startTracing() {
this.isTracing = true;
this.dbg.onEnterFrame = this.onEnterFrame;
if (this.traceDOMEvents) {
this.startTracingDOMEvents();
}
// In any case, we consider the tracing as started
this.notifyToggle(true);
}
startTracingDOMEvents() {
@ -242,7 +269,8 @@ class JavaScriptTracer {
* Optional string to justify why the tracer stopped.
*/
stopTracing(reason = "") {
if (!this.isTracing()) {
// Note that this may be called before `#startTracing()`, but still want to completely shut it down.
if (!this.dbg) {
return;
}
@ -250,8 +278,10 @@ class JavaScriptTracer {
this.dbg.removeAllDebuggees();
this.dbg.onNewGlobalObject = undefined;
this.dbg = null;
this.depth = 0;
this.options = null;
// Cancel the traceOnNextInteraction event listeners.
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
@ -262,14 +292,11 @@ class JavaScriptTracer {
}
this.tracedGlobal = null;
this.isTracing = false;
this.notifyToggle(false, reason);
}
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.
@ -597,7 +624,7 @@ function addTracingListener(listener) {
listeners.add(listener);
if (
activeTracer?.isTracing() &&
activeTracer?.isTracing &&
typeof listener.onTracingToggled == "function"
) {
listener.onTracingToggled(true);

Просмотреть файл

@ -4,19 +4,16 @@
"use strict";
loader.lazyGetter(this, "TARGET_TYPES", function () {
return require("resource://devtools/shared/commands/target/target-command.js")
.TYPES;
});
class TracerCommand {
constructor({ commands }) {
this.#targetCommand = commands.targetCommand;
this.#targetConfigurationCommand = commands.targetConfigurationCommand;
this.#resourceCommand = commands.resourceCommand;
}
#resourceCommand;
#targetCommand;
#targetConfigurationCommand;
#isTracing = false;
async initialize() {
@ -41,6 +38,30 @@ class TracerCommand {
}
};
/**
* Get the dictionary passed to the server codebase as a SessionData.
* This contains all settings to fine tune the tracer actual behavior.
*
* @return {JSON}
* Configuration object.
*/
#getTracingOptions() {
return {
logMethod: Services.prefs.getStringPref(
"devtools.debugger.javascript-tracing-log-method",
""
),
traceValues: Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-values",
false
),
traceOnNextInteraction: Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-on-next-interaction",
false
),
};
}
/**
* Toggle JavaScript tracing for all targets.
*
@ -50,45 +71,9 @@ class TracerCommand {
async toggle(logMethod) {
this.#isTracing = !this.#isTracing;
// If no explicit log method is passed, default to the preference value.
if (!logMethod && this.#isTracing) {
logMethod = Services.prefs.getStringPref(
"devtools.debugger.javascript-tracing-log-method",
""
);
}
const traceValues = Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-values",
false
);
const traceOnNextInteraction = Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-on-next-interaction",
false
);
const targets = this.#targetCommand.getAllTargets(
this.#targetCommand.ALL_TYPES
);
await Promise.all(
targets.map(async targetFront => {
const tracerFront = await targetFront.getFront("tracer");
// Bug 1848136: For now the tracer doesn't work for worker targets.
if (tracerFront.targetType == TARGET_TYPES.WORKER) {
return null;
}
if (this.#isTracing) {
return tracerFront.startTracing(logMethod, {
traceValues,
traceOnNextInteraction,
});
}
return tracerFront.stopTracing();
})
);
await this.#targetConfigurationCommand.updateConfiguration({
tracerOptions: this.#isTracing ? this.#getTracingOptions() : undefined,
});
}
}