Bug 1803616 - [devtools] Implement a Javascript Tracer in the debugger. r=devtools-reviewers,devtools-backward-compat-reviewers,nchevobbe

On the server side, this patch introduces:
* a new "tracer" Target Scope actor to start and stop tracing per target
* a new TRACING_STATE resource in order to report to the client when we start/stop tracing and with which log method.

On the frontend side, this patch introduces:
* a global tracer button, which will enable/disable tracing for all targets/threads
  all at once.
* a global header, similar to pause, reporting if any target is tracing or not.
The header reuses the pause header and we may want to followup to better coordinate case
where we pause and trace at the same time. Only one of the two states is displayed.

We may want to followup here to be able to trace only one target and see the state per target.

Differential Revision: https://phabricator.services.mozilla.com/D163614
This commit is contained in:
Alexandre Poirot 2023-03-14 17:17:05 +00:00
parent 7d276f75c6
commit 5cb1635d84
46 changed files with 770 additions and 26 deletions

View File

@ -2533,7 +2533,7 @@
"byName": {},
"byBlocks": {},
"usedIds": {
"0": 0
"1": 1
}
}
}
@ -2554,7 +2554,7 @@
"byName": {},
"byBlocks": {},
"usedIds": {
"0": 0
"1": 1
}
}
}

View File

@ -779,7 +779,7 @@ function findSourceMatches(sourceId, content, queryText, modifiers) {
const startRegex = /([ !@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/g; // Similarly, find
const endRegex = new RegExp(["([ !@#$%^&*()_+-=[]{};':\"\\|,.<>/?])", '[^ !@#$%^&*()_+-=[]{};\':"\\|,.<>/?]*$"/'].join("")); // For texts over 100 characterss this truncates the text (for display)
const endRegex = new RegExp(["([ !@#$%^&*()_+-=[]{};':\"\\|,.<>/?])", '[^ !@#$%^&*()_+-=[]{};\':"\\|,.<>/?]*$"/'].join("")); // For texts over 100 characters this truncates the text (for display)
// around the context of the matched text.
function truncateLine(text, column) {

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill" fill-rule="evenodd" clip-rule="evenodd" d="M7.99873 15C11.8647 15 14.9987 11.866 14.9987 8C14.9987 4.13401 11.8647 1 7.99873 1C4.13273 1 0.998726 4.13401 0.998726 8C0.998726 11.866 4.13273 15 7.99873 15ZM7.99872 1.75C8.41294 1.75 8.74872 2.08579 8.74872 2.5V8.5C8.74872 8.72784 8.64516 8.94332 8.46725 9.08565L5.96725 11.0857C5.6438 11.3444 5.17183 11.292 4.91307 10.9685C4.65432 10.6451 4.70676 10.1731 5.0302 9.91435L7.24872 8.13953V2.5C7.24872 2.08579 7.58451 1.75 7.99872 1.75ZM5.49873 2.75C5.91294 2.75 6.24873 3.08579 6.24873 3.5V7C6.24873 7.22784 6.14516 7.44332 5.96725 7.58565L3.46725 9.58565C3.1438 9.84441 2.67183 9.79197 2.41307 9.46852C2.15432 9.14507 2.20676 8.67311 2.5302 8.41435L4.74873 6.63953V3.5C4.74873 3.08579 5.08451 2.75 5.49873 2.75ZM11.25 9C11.25 8.58579 10.9142 8.25 10.5 8.25C10.0858 8.25 9.75 8.58579 9.75 9V12C9.75 12.4142 10.0858 12.75 10.5 12.75C10.9142 12.75 11.25 12.4142 11.25 12V9ZM8.74872 11.5C8.74873 11.0858 8.41294 10.75 7.99873 10.75C7.58451 10.75 7.24873 11.0858 7.24872 11.5V13C7.24872 13.4142 7.58451 13.75 7.99872 13.75C8.41294 13.75 8.74872 13.4142 8.74872 13V11.5ZM11.2487 3.5C11.2487 3.08579 10.9129 2.75 10.4987 2.75C10.0845 2.75 9.74873 3.08579 9.74873 3.5V6C9.74873 6.22784 9.8523 6.44332 10.0302 6.58565L12.2487 8.36047V10C12.2487 10.4142 12.5845 10.75 12.9987 10.75C13.4129 10.75 13.7487 10.4142 13.7487 10V8C13.7487 7.77216 13.6452 7.55668 13.4672 7.41435L11.2487 5.63953V3.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -20,6 +20,7 @@ import * as tabs from "./tabs";
import * as threads from "./threads";
import * as toolbox from "./toolbox";
import * as preview from "./preview";
import * as tracing from "./tracing";
import { objectInspector } from "devtools/client/shared/components/reps/index";
@ -43,4 +44,5 @@ export default {
...threads,
...toolbox,
...preview,
...tracing,
};

View File

@ -25,6 +25,7 @@ CompiledModules(
"source-tree.js",
"tabs.js",
"toolbox.js",
"tracing.js",
"threads.js",
"ui.js",
)

View File

@ -0,0 +1,49 @@
/* 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/>. */
import { getIsThreadCurrentlyTracing, getAllThreads } from "../selectors";
import { PROMISE } from "./utils/middleware/promise";
/**
* Toggle ON/OFF Javascript tracing for all targets,
* using the specified log method.
*
* @param {string} logMethod
* Can be "stdout" or "console". See TracerActor.
*/
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)
);
// Automatically open the split console when enabling tracing to the console
if (!isTracingEnabled && logMethod == "console") {
await panel.toolbox.openSplitConsole();
}
return dispatch({
type: "TOGGLE_TRACING",
[PROMISE]: isTracingEnabled
? client.stopTracing()
: client.startTracing(logMethod),
});
};
}
/**
* Called when tracing is toggled ON/OFF on a particular thread.
*/
export function tracingToggled(thread, enabled) {
return ({ dispatch }) => {
dispatch({
type: "TRACING_TOGGLED",
thread,
enabled,
});
};
}

View File

@ -81,6 +81,9 @@ export async function onConnect(_commands, _resourceCommand, _actions, store) {
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: onThreadStateAvailable,
});
await resourceCommand.watchResources([resourceCommand.TYPES.TRACING_STATE], {
onAvailable: onTracingStateAvailable,
});
await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
onAvailable: actions.addExceptionFromResources,
@ -104,6 +107,9 @@ export function onDisconnect() {
resourceCommand.unwatchResources([resourceCommand.TYPES.THREAD_STATE], {
onAvailable: onThreadStateAvailable,
});
resourceCommand.unwatchResources([resourceCommand.TYPES.TRACING_STATE], {
onAvailable: onTracingStateAvailable,
});
resourceCommand.unwatchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
onAvailable: actions.addExceptionFromResources,
});
@ -173,6 +179,16 @@ async function onThreadStateAvailable(resources) {
}
}
async function onTracingStateAvailable(resources) {
for (const resource of resources) {
if (resource.targetFront.isDestroyed()) {
continue;
}
const threadFront = await resource.targetFront.getFront("thread");
await actions.tracingToggled(threadFront.actor, resource.enabled);
}
}
function onDocumentEventAvailable(events) {
for (const event of events) {
// Only consider top level document, and ignore remote iframes top document

View File

@ -100,6 +100,51 @@ function forEachThread(iteratee) {
return Promise.all(promises);
}
/**
* Start JavaScript tracing for all targets.
*
* @param {String} logMethod
* Where to log the traces. Can be stdout or console.
*/
async function startTracing(logMethod) {
// Ignore the request if the server doesn't support this feature yet
// @backward-compat { version 112 } Fx 112 started implementing JS tracing.
// The trait can be removed once this version is released.
if (!commands.client.mainRoot.traits.supportsJavascriptTracing) {
return;
}
const targets = commands.targetCommand.getAllTargets(
commands.targetCommand.ALL_TYPES
);
await Promise.all(
targets.map(async targetFront => {
const tracerFront = await targetFront.getFront("tracer");
return tracerFront.startTracing(logMethod);
})
);
}
/**
* Stop JavaScript tracing for all targets.
*/
async function stopTracing() {
// Ignore the request if the server doesn't support this feature yet
// @backward-compat { version 112 } Fx 112 started implementing JS tracing.
// The trait can be removed once this version is released.
if (!commands.client.mainRoot.traits.supportsJavascriptTracing) {
return;
}
const targets = commands.targetCommand.getAllTargets(
commands.targetCommand.ALL_TYPES
);
await Promise.all(
targets.map(async targetFront => {
const tracerFront = await targetFront.getFront("tracer");
return tracerFront.stopTracing();
})
);
}
function resume(thread, frameId) {
return lookupThreadFront(thread).resume();
}
@ -428,6 +473,8 @@ const clientCommands = {
loadObjectProperties,
releaseActor,
pauseGrip,
startTracing,
stopTracing,
resume,
stepIn,
stepOut,

View File

@ -15,12 +15,15 @@ import {
isTopFrameSelected,
getThreadContext,
getIsCurrentThreadPaused,
getIsThreadCurrentlyTracing,
getSupportsJavascriptTracing,
} from "../../selectors";
import { formatKeyShortcut } from "../../utils/text";
import actions from "../../actions";
import { debugBtn } from "../shared/Button/CommandBarButton";
import AccessibleImage from "../shared/AccessibleImage";
import "./CommandBar.css";
import { showMenu } from "../../context-menu/menu";
const MenuButton = require("devtools/client/shared/components/menu/MenuButton");
const MenuItem = require("devtools/client/shared/components/menu/MenuItem");
@ -54,6 +57,11 @@ const KEYS = {
},
};
const LOG_METHODS = {
CONSOLE: "console",
STDOUT: "stdout",
};
function getKey(action) {
return getKeyForOS(Services.appinfo.OS, action);
}
@ -75,6 +83,13 @@ function formatKey(action) {
}
class CommandBar extends Component {
constructor() {
super();
this.state = {
logMethod: LOG_METHODS.CONSOLE,
};
}
static get propTypes() {
return {
breakOnNext: PropTypes.func.isRequired,
@ -83,6 +98,7 @@ class CommandBar extends Component {
isPaused: PropTypes.bool.isRequired,
isWaitingOnBreak: PropTypes.bool.isRequired,
javascriptEnabled: PropTypes.bool.isRequired,
trace: PropTypes.func.isRequired,
resume: PropTypes.func.isRequired,
skipPausing: PropTypes.bool.isRequired,
stepIn: PropTypes.func.isRequired,
@ -94,6 +110,7 @@ class CommandBar extends Component {
toggleSkipPausing: PropTypes.any.isRequired,
toggleSourceMapsEnabled: PropTypes.func.isRequired,
topFrameSelected: PropTypes.bool.isRequired,
toggleTracing: PropTypes.func.isRequired,
};
}
@ -142,6 +159,7 @@ class CommandBar extends Component {
const isDisabled = !isPaused;
return [
this.renderTraceButton(),
this.renderPauseButton(),
debugBtn(
() => this.props.stepOver(),
@ -171,6 +189,63 @@ class CommandBar extends Component {
this.props.resume();
}
renderTraceButton() {
if (!features.javascriptTracing || !this.props.supportsJavascriptTracing) {
return null;
}
// 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 loggin output (console or stdout)
return (
<button
className={`devtools-button command-bar-button debugger-trace-menu-button ${
this.props.isTracingEnabled ? "active" : ""
}`}
title={
this.props.isTracingEnabled
? L10N.getStr("stopTraceButtonTooltip")
: L10N.getFormatStr("startTraceButtonTooltip", this.state.logMethod)
}
onClick={event => {
this.props.toggleTracing(this.state.logMethod);
}}
onContextMenu={event => {
event.preventDefault();
event.stopPropagation();
// Avoid showing the menu to avoid having to support chaging tracing config "live"
if (this.props.isTracingEnabled) {
return;
}
const items = [
{
id: "debugger-trace-menu-item-console",
label: L10N.getStr("traceInWebConsole"),
checked: this.state.logMethod == LOG_METHODS.CONSOLE,
click: () => {
this.setState({
logMethod: LOG_METHODS.CONSOLE,
});
},
},
{
id: "debugger-trace-menu-item-stdout",
label: L10N.getStr("traceInStdout"),
checked: this.state.logMethod == LOG_METHODS.STDOUT,
click: () => {
this.setState({
logMethod: LOG_METHODS.STDOUT,
});
},
},
];
showMenu(event, items);
}}
/>
);
}
renderPauseButton() {
const { cx, breakOnNext, isWaitingOnBreak } = this.props;
@ -313,9 +388,12 @@ const mapStateToProps = state => ({
topFrameSelected: isTopFrameSelected(state, getCurrentThread(state)),
javascriptEnabled: state.ui.javascriptEnabled,
isPaused: getIsCurrentThreadPaused(state),
isTracingEnabled: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
supportsJavascriptTracing: getSupportsJavascriptTracing(state),
});
export default connect(mapStateToProps, {
toggleTracing: actions.toggleTracing,
resume: actions.resume,
stepIn: actions.stepIn,
stepOut: actions.stepOut,

View File

@ -52,3 +52,10 @@
.devtools-button.debugger-settings-menu-button::before {
background-image: url("chrome://devtools/skin/images/settings.svg");
}
.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 {
fill: var(--theme-icon-checked-color);
}

View File

@ -18,6 +18,7 @@ import {
import { initialBreakpointsState } from "./reducers/breakpoints";
import { initialSourcesState } from "./reducers/sources";
import { initialUIState } from "./reducers/ui";
const { sanitizeBreakpoints } = require("devtools/client/shared/thread-utils");
async function syncBreakpoints() {
@ -54,7 +55,7 @@ function setPauseOnExceptions() {
);
}
async function loadInitialState() {
async function loadInitialState(commands) {
const pendingBreakpoints = sanitizeBreakpoints(
await asyncStore.pendingBreakpoints
);
@ -64,6 +65,10 @@ async function loadInitialState() {
const eventListenerBreakpoints = await asyncStore.eventListenerBreakpoints;
const breakpoints = initialBreakpointsState(xhrBreakpoints);
const sources = initialSourcesState({ blackboxedRanges });
const ui = initialUIState({
supportsJavascriptTracing:
commands.client.mainRoot.traits.supportsJavascriptTracing,
});
return {
pendingBreakpoints,
@ -71,6 +76,7 @@ async function loadInitialState() {
breakpoints,
eventListenerBreakpoints,
sources,
ui,
};
}
@ -83,7 +89,7 @@ export async function bootstrap({
}) {
verifyPrefSchema();
const initialState = await loadInitialState();
const initialState = await loadInitialState(commands);
const workers = bootstrapWorkers(panelWorkers);
const { store, actions, selectors } = bootstrapStore(

View File

@ -10,6 +10,10 @@
export function initialThreadsState() {
return {
threads: [],
// List of thread actor IDs which are current tracing.
// i.e. where JavaScript tracing is enabled.
mutableTracingThreads: new Set(),
};
}
@ -28,18 +32,37 @@ export default function update(state = initialThreadsState(), action) {
thread => action.threadActorID != thread.actor
),
};
case "UPDATE_SERVICE_WORKER_STATUS":
const { thread, status } = action;
return {
...state,
threads: state.threads.map(t => {
if (t.actor == thread) {
return { ...t, serviceWorkerStatus: status };
if (t.actor == action.thread) {
return { ...t, serviceWorkerStatus: action.status };
}
return t;
}),
};
case "TRACING_TOGGLED":
const { mutableTracingThreads } = state;
const sizeBefore = mutableTracingThreads.size;
if (action.enabled) {
mutableTracingThreads.add(action.thread);
} else {
mutableTracingThreads.delete(action.thread);
}
// We may receive toggle events when we change the logging method
// while we are already tracing, but the list of tracing thread stays the same.
const changed = mutableTracingThreads.size != sizeBefore;
if (changed) {
return {
...state,
mutableTracingThreads,
};
}
return state;
default:
return state;
}

View File

@ -11,7 +11,7 @@
import { prefs, features } from "../utils/prefs";
export const initialUIState = () => ({
export const initialUIState = ({ supportsJavascriptTracing = false } = {}) => ({
selectedPrimaryPaneTab: "sources",
activeSearch: null,
startPanelCollapsed: prefs.startPanelCollapsed,
@ -26,6 +26,7 @@ export const initialUIState = () => ({
inlinePreviewEnabled: features.inlinePreview,
editorWrappingEnabled: prefs.editorWrapping,
javascriptEnabled: true,
supportsJavascriptTracing,
});
function update(state = initialUIState(), action) {

View File

@ -50,3 +50,7 @@ export function getMainThreadHost(state) {
export function getThread(state, threadActor) {
return getAllThreads(state).find(thread => thread.actor === threadActor);
}
export function getIsThreadCurrentlyTracing(state, thread) {
return state.threads.mutableTracingThreads.has(thread);
}

View File

@ -53,3 +53,7 @@ export function getInlinePreview(state) {
export function getEditorWrapping(state) {
return state.ui.editorWrappingEnabled;
}
export function getSupportsJavascriptTracing(state) {
return state.ui.supportsJavascriptTracing;
}

View File

@ -72,6 +72,7 @@ if (isNode()) {
pref("devtools.debugger.features.overlay-step-buttons", true);
pref("devtools.debugger.features.frame-step", true);
pref("devtools.debugger.features.blackbox-lines", false);
pref("devtools.debugger.features.javascript-tracing", false);
pref("devtools.editor.tabsize", 2);
}
@ -158,6 +159,7 @@ export const features = new PrefsHelper("devtools.debugger.features", {
windowlessServiceWorkers: ["Bool", "windowless-service-workers"],
frameStep: ["Bool", "frame-step"],
blackboxLines: ["Bool", "blackbox-lines"],
javascriptTracing: ["Bool", "javascript-tracing"],
});
// Import the asyncStore already spawned by the TargetMixin class

View File

@ -282,3 +282,4 @@ skip-if = asan || !nightly_build || fission # Bug 1591064, parent intercept mode
[browser_dbg-project-root.js]
[browser_dbg-project-search.js]
[browser_dbg-sources-project-search.js]
[browser_dbg-javascript-tracer.js]

View File

@ -0,0 +1,64 @@
/* 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/>. */
// Tests the Javascript Tracing feature.
"use strict";
add_task(async function() {
// This is preffed off for now, so ensure turning it on
await pushPref("devtools.debugger.features.javascript-tracing", true);
const dbg = await initDebugger("doc-scripts.html");
info("Enable the tracing");
await clickElement(dbg, "trace");
const topLevelThread =
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
info("Wait for tracing to be enabled");
await waitForState(dbg, state => {
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThread);
});
ok(
dbg.toolbox.splitConsole,
"Split console is automatically opened when tracing to the console"
);
invokeInTab("main");
info("Wait for console messages for the whole trace");
// `main` calls `foo` which calls `bar`
await hasConsoleMessage(dbg, "λ main");
await hasConsoleMessage(dbg, "λ foo");
await hasConsoleMessage(dbg, "λ bar");
const traceMessages = await findConsoleMessages(dbg.toolbox, "λ main");
is(traceMessages.length, 1, "We got a unique trace for 'main' function call");
const sourceLink = traceMessages[0].querySelector(".frame-link-source");
sourceLink.click();
info("Wait for the main function to be highlighted in the debugger");
await waitForSelectedSource(dbg, "simple1.js");
await waitForSelectedLocation(dbg, 1, 16);
info("Disable the tracing");
await clickElement(dbg, "trace");
info("Wait for tracing to be disabled");
await waitForState(dbg, state => {
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThread);
});
invokeInTab("inline_script2");
// Let some time for the tracer to appear if we failed disabling the tracing
await wait(1000);
const messages = await findConsoleMessages(dbg.toolbox, "inline_script2");
is(
messages.length,
0,
"We stopped recording traces, an the function call isn't logged in the console"
);
});

View File

@ -1576,8 +1576,7 @@ const selectors = {
stepOver: ".stepOver.active",
stepOut: ".stepOut.active",
stepIn: ".stepIn.active",
replayPrevious: ".replay-previous.active",
replayNext: ".replay-next.active",
trace: ".debugger-trace-menu-button",
prettyPrintButton: ".source-footer .prettyPrint",
sourceMapLink: ".source-footer .mapped-source",
sourcesFooter: ".sources-panel .source-footer",

View File

@ -101,6 +101,7 @@ class WorkerDescriptorFront extends DescriptorMixin(
// Set the console and thread actor IDs on the form so it is accessible by TargetMixin.getFront
this.targetForm.consoleActor = workerTargetForm.consoleActor;
this.targetForm.threadActor = workerTargetForm.threadActor;
this.targetForm.tracerActor = workerTargetForm.tracerActor;
if (this.isDestroyedOrBeingDestroyed()) {
return this;

View File

@ -52,6 +52,7 @@ DevToolsModules(
"target-configuration.js",
"thread-configuration.js",
"thread.js",
"tracer.js",
"walker.js",
"watcher.js",
"webconsole.js",

View File

@ -0,0 +1,22 @@
/* 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/. */
"use strict";
const {
FrontClassWithSpec,
registerFront,
} = require("resource://devtools/shared/protocol.js");
const { tracerSpec } = require("resource://devtools/shared/specs/tracer.js");
class TracerFront extends FrontClassWithSpec(tracerSpec) {
constructor(client, targetFront, parentFront) {
super(client, targetFront, parentFront);
// Attribute name from which to retrieve the actorID out of the target actor's form
this.formAttributeName = "tracerActor";
}
}
registerFront(TracerFront);

View File

@ -249,6 +249,7 @@ devtools.jar:
content/debugger/images/stepIn.svg (debugger/images/stepIn.svg)
content/debugger/images/stepOut.svg (debugger/images/stepOut.svg)
content/debugger/images/tab.svg (debugger/images/tab.svg)
content/debugger/images/trace.svg (debugger/images/trace.svg)
content/debugger/images/webconsole-logpoint.svg (debugger/images/webconsole-logpoint.svg)
content/debugger/images/whole-word-match.svg (debugger/images/whole-word-match.svg)
content/debugger/images/window.svg (debugger/images/window.svg)

View File

@ -120,6 +120,25 @@ pauseButtonTooltip=Pause %S
# the pause button after it's been clicked but before the next JavaScript to run.
pausePendingButtonTooltip=Waiting for next execution
# LOCALIZATION NOTE (startTraceButtonTooltip): The label that is displayed on the trace
# button in the top of the debugger right sidebar. %S is for the log output location (webconsole or stdout).
startTraceButtonTooltip=Trace all JavaScript frames to %S.\nRight click to change the output.
# LOCALIZATION NOTE (stopTraceButtonTooltip): The label that is displayed on the trace
# button in the top of the debugger right sidebar. This label is only displayed when we are current tracing
# JavaScript.
stopTraceButtonTooltip=Stop tracing JavaScript frames.
# LOCALIZATION NOTE (traceInWebConsole): The label that is displayed in the context menu
# of the trace button, which is in the top of the debugger right sidebar.
# This is used to force logging JavaScript traces in the Web Console.
traceInWebConsole=Trace in the web console
# LOCALIZATION NOTE (traceInWebConsole): The label that is displayed in the context menu
# of the trace button, which is in the top of the debugger right sidebar.
# This is used to force logging JavaScript traces in the stdout.
traceInStdout=Trace in the stdout
# LOCALIZATION NOTE (resumeButtonTooltip): The label that is displayed on the pause
# button when the debugger is in a paused state.
resumeButtonTooltip=Resume %S

View File

@ -134,6 +134,10 @@ level.debug=Debug
# Tooltip shown for logpoints sent from the debugger
logpoint.title=Logpoints from the debugger
# LOCALIZATION NOTE (logtrace.title)
# Tooltip shown for JavaScript tracing logs
logtrace.title=JavaScript tracing
# LOCALIZATION NOTE (blockedReason.title)
# Tooltip shown for blocked network events sent from the network panel
blockedrequest.label=Blocked by DevTools

View File

@ -84,3 +84,4 @@ pref("devtools.debugger.features.overlay", true);
pref("devtools.debugger.features.inline-preview", true);
pref("devtools.debugger.features.frame-step", true);
pref("devtools.debugger.features.blackbox-lines", false);
pref("devtools.debugger.features.javascript-tracing", false);

View File

@ -311,6 +311,12 @@
stroke: var(--theme-graphs-purple);
}
.message > .icon.logtrace {
background-image: url(chrome://devtools/content/debugger/images/trace.svg);
-moz-context-properties: fill, stroke;
fill: var(--theme-icon-checked-color);
}
.message.network-message-blocked > .icon {
color: var(--theme-icon-error-color);
background-image: url(chrome://devtools/skin/images/blocked.svg);

View File

@ -32,12 +32,13 @@ function getIconElement(level, type, title) {
title = title || l10n.getStr(l10nLevels[level] || level);
const classnames = ["icon"];
if (type && type === "logPoint") {
if (type === "logPoint") {
title = l10n.getStr("logpoint.title");
classnames.push("logpoint");
}
if (type && type === "blockedReason") {
} else if (type === "logTrace") {
title = l10n.getStr("logtrace.title");
classnames.push("logtrace");
} else if (type === "blockedReason") {
title = l10n.getStr("blockedrequest.label");
}

View File

@ -63,8 +63,11 @@ class WorkerDescriptorActor extends Actor {
form() {
const form = {
actor: this.actorID,
consoleActor: this._consoleActor,
threadActor: this._threadActor,
tracerActor: this._tracerActor,
id: this._dbg.id,
url: this._dbg.url,
traits: {},
@ -115,8 +118,10 @@ class WorkerDescriptorActor extends Actor {
if (this._threadActor !== null) {
return {
type: "connected",
threadActor: this._threadActor,
consoleActor: this._consoleActor,
threadActor: this._threadActor,
tracerActor: this._tracerActor,
};
}
@ -130,14 +135,19 @@ class WorkerDescriptorActor extends Actor {
}
);
this._threadActor = workerTargetForm.threadActor;
this._consoleActor = workerTargetForm.consoleActor;
this._threadActor = workerTargetForm.threadActor;
this._tracerActor = workerTargetForm.tracerActor;
this._transport = transport;
return {
type: "connected",
threadActor: this._threadActor,
consoleActor: this._consoleActor,
threadActor: this._threadActor,
tracerActor: this._tracerActor,
url: this._dbg.url,
};
} catch (error) {

View File

@ -59,6 +59,7 @@ DevToolsModules(
"target-configuration.js",
"thread-configuration.js",
"thread.js",
"tracer.js",
"watcher.js",
"webbrowser.js",
"webconsole.js",

View File

@ -112,14 +112,14 @@ class ConsoleMessageWatcher {
}
/**
* Called by devtools/server/actors/utils/logEvent.js, whenever a new
* log point is triggered and request to spawn a console message
* Spawn a custom console message.
* This is used for example for log points and JS tracing.
*
* @param Object message
* A fake nsIConsoleMessage, which looks like the one being generated by
* the platform API.
*/
onLogPoint(message) {
emitMessage(message) {
if (!this.listener) {
throw new Error("This target actor isn't listening to console messages");
}

View File

@ -21,6 +21,7 @@ const TYPES = {
SOURCE: "source",
STYLESHEET: "stylesheet",
THREAD_STATE: "thread-state",
TRACING_STATE: "tracing-state",
WEBSOCKET: "websocket",
// storage types
@ -98,6 +99,9 @@ const FrameTargetResources = augmentResourceDictionary({
[TYPES.THREAD_STATE]: {
path: "devtools/server/actors/resources/thread-states",
},
[TYPES.TRACING_STATE]: {
path: "devtools/server/actors/resources/tracing-state",
},
[TYPES.SERVER_SENT_EVENT]: {
path: "devtools/server/actors/resources/server-sent-events",
},
@ -126,6 +130,9 @@ const ProcessTargetResources = augmentResourceDictionary({
[TYPES.THREAD_STATE]: {
path: "devtools/server/actors/resources/thread-states",
},
[TYPES.TRACING_STATE]: {
path: "devtools/server/actors/resources/tracing-state",
},
});
// Worker target resources are spawned via a Worker Target Actor.
@ -147,6 +154,9 @@ const WorkerTargetResources = augmentResourceDictionary({
[TYPES.THREAD_STATE]: {
path: "devtools/server/actors/resources/thread-states",
},
[TYPES.TRACING_STATE]: {
path: "devtools/server/actors/resources/tracing-state",
},
});
// Parent process resources are spawned via the Watcher Actor.

View File

@ -34,6 +34,7 @@ DevToolsModules(
"storage-session-storage.js",
"stylesheets.js",
"thread-states.js",
"tracing-state.js",
"websockets.js",
)

View File

@ -0,0 +1,54 @@
/* 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/. */
"use strict";
const {
TYPES: { TRACING_STATE },
} = require("resource://devtools/server/actors/resources/index.js");
class TracingStateWatcher {
/**
* Start watching for tracing state changes for a given target actor.
*
* @param TargetActor targetActor
* The target actor from which we should observe
* @param Object options
* Dictionary object with following attributes:
* - onAvailable: mandatory function
* This will be called for each resource.
*/
async watch(targetActor, { onAvailable }) {
this.onAvailable = onAvailable;
// Ensure dispatching the existing state, only if a tracer is enabled.
const tracerActor = targetActor.getTargetScopedActor("tracer");
if (!tracerActor || !tracerActor.isTracing()) {
return;
}
this.onTracingToggled(true, tracerActor.getLogMethod());
}
/**
* Stop watching for tracing state
*/
destroy() {}
// Emit a TRACING_STATE resource with:
// enabled = true|false
// logMethod = console|stdout
// When Javascript tracing is enabled or disabled.
onTracingToggled(enabled, logMethod) {
this.onAvailable([
{
resourceType: TRACING_STATE,
enabled,
logMethod,
},
]);
}
}
module.exports = TracingStateWatcher;

View File

@ -136,6 +136,9 @@ class RootActor extends Actor {
"dom.worker.console.dispatch_events_to_main_thread"
)
: true,
// @backward-compat { version 112 } Fx 112 started implementing JS tracing.
// The trait can be removed once this version is released.
supportsJavascriptTracing: true,
};
}

View File

@ -93,5 +93,22 @@ class BaseTargetActor extends Actor {
resource => (resource.browsingContextID = browsingContextID)
);
}
/**
* Try to return any target scoped actor instance, if it exists.
* They are lazily instantiated and so will only be available
* if the client called at least one of their method.
*
* @param {String} prefix
* Prefix for the actor we would like to retrieve.
* Defined in devtools/server/actors/utils/actor-registry.js
*/
getTargetScopedActor(prefix) {
if (this.isDestroyed()) {
return null;
}
const form = this.form();
return this.conn._getOrCreateActor(form[prefix + "Actor"]);
}
}
exports.BaseTargetActor = BaseTargetActor;

View File

@ -48,6 +48,12 @@ loader.lazyRequireGetter(
"resource://devtools/server/actors/memory.js",
true
);
loader.lazyRequireGetter(
this,
"TracerActor",
"resource://devtools/server/actors/tracer.js",
true
);
class ContentProcessTargetActor extends BaseTargetActor {
constructor(conn, { isXpcShellTarget = false, sessionContext } = {}) {
@ -149,15 +155,21 @@ class ContentProcessTargetActor extends BaseTargetActor {
this.memoryActor = new MemoryActor(this.conn, this);
this.manage(this.memoryActor);
}
if (!this.tracerActor) {
this.tracerActor = new TracerActor(this.conn, this);
this.manage(this.tracerActor);
}
return {
actor: this.actorID,
consoleActor: this._consoleActor.actorID,
isXpcShellTarget: this.isXpcShellTarget,
memoryActor: this.memoryActor.actorID,
processID: Services.appinfo.processID,
remoteType: Services.appinfo.remoteType,
consoleActor: this._consoleActor.actorID,
memoryActor: this.memoryActor.actorID,
threadActor: this.threadActor.actorID,
tracerActor: this.tracerActor.actorID,
traits: {
networkMonitor: false,

View File

@ -8,10 +8,12 @@ const {
workerTargetSpec,
} = require("resource://devtools/shared/specs/targets/worker.js");
const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
const {
WebConsoleActor,
} = require("resource://devtools/server/actors/webconsole.js");
const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
const { TracerActor } = require("resource://devtools/server/actors/tracer.js");
const Targets = require("resource://devtools/server/actors/targets/index.js");
const makeDebuggerUtil = require("resource://devtools/server/actors/utils/make-debugger.js");
@ -63,8 +65,11 @@ class WorkerTargetActor extends BaseTargetActor {
// needed by the thread actor to communicate with the console when evaluating logpoints.
this._consoleActor = new WebConsoleActor(this.conn, this);
this.tracerActor = new TracerActor(this.conn, this);
this.manage(this.threadActor);
this.manage(this._consoleActor);
this.manage(this.tracerActor);
}
// Expose the worker URL to the thread actor.
@ -76,8 +81,11 @@ class WorkerTargetActor extends BaseTargetActor {
form() {
return {
actor: this.actorID,
threadActor: this.threadActor?.actorID,
consoleActor: this._consoleActor?.actorID,
threadActor: this.threadActor?.actorID,
tracerActor: this.tracerActor?.actorID,
id: this._workerDebuggerData.id,
type: this._workerDebuggerData.type,
url: this._workerDebuggerData.url,

View File

@ -0,0 +1,222 @@
/* 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/. */
"use strict";
const { Actor } = require("resource://devtools/shared/protocol.js");
const { tracerSpec } = require("resource://devtools/shared/specs/tracer.js");
const {
TYPES,
getResourceWatcher,
} = require("resource://devtools/server/actors/resources/index.js");
const LOG_METHODS = {
STDOUT: "stdout",
CONSOLE: "console",
};
const CONSOLE_ARGS_STYLES = [
"color: var(--theme-toolbarbutton-checked-hover-background)",
"padding-inline: 4px; margin-inline: 2px; background-color: var(--theme-toolbarbutton-checked-hover-background); color: var(--theme-toolbarbutton-checked-hover-color);",
"",
"color: var(--theme-highlight-blue); margin-inline: 2px;",
];
class TracerActor extends Actor {
constructor(conn, targetActor) {
super(conn, tracerSpec);
this.targetActor = targetActor;
this.onEnterFrame = this.onEnterFrame.bind(this);
}
isTracing() {
return !!this.dbg;
}
getLogMethod() {
return this.logMethod;
}
startTracing(logMethod = LOG_METHODS.STDOUT) {
this.logMethod = logMethod;
// If we are already recording traces, only change the log method and notify about
// the new log method to the client.
if (!this.isTracing()) {
// Instantiate a brand new Debugger API so that we can trace independently
// of all other debugger operations. i.e. we can pause while tracing without any interference.
this.dbg = this.targetActor.makeDebugger();
this.depth = 0;
if (this.logMethod == LOG_METHODS.STDOUT) {
dump("Start tracing JavaScript\n");
}
this.dbg.onEnterFrame = this.onEnterFrame;
this.dbg.enable();
}
const tracingStateWatcher = getResourceWatcher(
this.targetActor,
TYPES.TRACING_STATE
);
if (tracingStateWatcher) {
tracingStateWatcher.onTracingToggled(true, logMethod);
}
}
stopTracing() {
if (!this.isTracing()) {
return;
}
if (this.logMethod == LOG_METHODS.STDOUT) {
dump("Stop tracing JavaScript\n");
}
this.dbg.onEnterFrame = undefined;
this.dbg.disable();
this.dbg = null;
this.depth = 0;
const tracingStateWatcher = getResourceWatcher(
this.targetActor,
TYPES.TRACING_STATE
);
if (tracingStateWatcher) {
tracingStateWatcher.onTracingToggled(false);
}
}
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) {
const message =
"Looks like an infinite recursion? We stopped the JavaScript tracer, but code may still be running!";
if (this.logMethod == LOG_METHODS.STDOUT) {
dump(message + "\n");
} else if (this.logMethod == LOG_METHODS.CONSOLE) {
logConsoleMessage({
targetActor: this.targetActor,
source: script.source,
args: [message],
styles: [],
});
}
this.stopTracing();
return;
}
const { script } = frame;
const { lineNumber, columnNumber } = script.getOffsetMetadata(
frame.offset
);
if (this.logMethod == LOG_METHODS.STDOUT) {
const padding = "—".repeat(this.depth + 1);
const message = `${padding}[${frame.implementation}]—> ${
script.source.url
} @ ${lineNumber}:${columnNumber} - ${formatDisplayName(frame)}`;
dump(message + "\n");
} else if (this.logMethod == LOG_METHODS.CONSOLE) {
const args = [
"—".repeat(this.depth + 1),
frame.implementation,
"⟶",
formatDisplayName(frame),
];
logConsoleMessage({
targetActor: this.targetActor,
source: script.source,
lineNumber,
columnNumber,
args,
styles: CONSOLE_ARGS_STYLES,
});
}
this.depth++;
frame.onPop = () => {
this.depth--;
};
} catch (e) {
console.error("Exception while tracing javascript", e);
}
}
}
exports.TracerActor = TracerActor;
/**
* 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})`;
}
/**
* Log a message to the web console.
*
* @param {Object} targetActor
* The related target actor where we want to log the message.
* The message will be seen as coming from one specific target.
* @param {Debugger.Source} source
* The source of the location where you the message be reported to be coming from.
* @param {Number} line
* The line of the location where you the message be reported to be coming from.
* @param {Number} column
* The column of the location where you the message be reported to be coming from.
* @param {Array<Object>} args
* Arguments of the console message to display.
* @param {Object} styles
* Styles to apply to the console message.
*/
function logConsoleMessage({
targetActor,
source,
lineNumber,
columnNumber,
args,
styles,
}) {
const message = {
filename: source.url,
lineNumber,
columnNumber,
arguments: args,
styles,
level: "logTrace",
chromeContext:
targetActor.actorID &&
/conn\d+\.parentProcessTarget\d+/.test(targetActor.actorID),
// The 'prepareConsoleMessageForRemote' method in webconsoleActor expects internal source ID,
// thus we can't set sourceId directly to sourceActorID.
sourceId: source.id,
};
// Ignore the request if the frontend isn't listening to console messages for that target.
const consoleMessageWatcher = getResourceWatcher(
targetActor,
TYPES.CONSOLE_MESSAGE
);
if (consoleMessageWatcher) {
consoleMessageWatcher.emitMessage(message);
}
}

View File

@ -250,6 +250,11 @@ const ActorRegistry = {
constructor: "ScreenshotContentActor",
type: { target: true },
});
this.registerModule("devtools/server/actors/tracer", {
prefix: "tracer",
constructor: "TracerActor",
type: { target: true },
});
},
/**

View File

@ -93,7 +93,7 @@ function logEvent({ threadActor, frame, level, expression, bindings }) {
TYPES.CONSOLE_MESSAGE
);
if (consoleMessageWatcher) {
consoleMessageWatcher.onLogPoint(message);
consoleMessageWatcher.emitMessage(message);
} else {
// Bug 1642296: Once we enable ConsoleMessage resource on the server, we should remove onConsoleAPICall
// from the WebConsoleActor, and only support the ConsoleMessageWatcher codepath.

View File

@ -201,6 +201,7 @@ function getWatcherSupportedResources(type) {
[Resources.TYPES.THREAD_STATE]: true,
[Resources.TYPES.SERVER_SENT_EVENT]: true,
[Resources.TYPES.WEBSOCKET]: true,
[Resources.TYPES.TRACING_STATE]: true,
[Resources.TYPES.LAST_PRIVATE_CONTEXT_EXIT]: true,
};
}

View File

@ -1211,6 +1211,7 @@ ResourceCommand.TYPES = ResourceCommand.prototype.TYPES = {
REFLOW: "reflow",
SOURCE: "source",
THREAD_STATE: "thread-state",
TRACING_STATE: "tracing-state",
SERVER_SENT_EVENT: "server-sent-event",
LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit",
};

View File

@ -313,6 +313,11 @@ const Types = (exports.__TypesForTests = [
spec: "devtools/shared/specs/thread-configuration",
front: "devtools/client/fronts/thread-configuration",
},
{
types: ["tracer"],
spec: "devtools/shared/specs/tracer",
front: "devtools/client/fronts/tracer",
},
{
types: ["domwalker"],
spec: "devtools/shared/specs/walker",

View File

@ -57,6 +57,7 @@ DevToolsModules(
"target-configuration.js",
"thread-configuration.js",
"thread.js",
"tracer.js",
"walker.js",
"watcher.js",
"webconsole.js",

View File

@ -0,0 +1,27 @@
/* 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/. */
"use strict";
const {
generateActorSpec,
Arg,
} = require("resource://devtools/shared/protocol.js");
const tracerSpec = generateActorSpec({
typeName: "tracer",
methods: {
startTracing: {
request: {
logMethod: Arg(0, "string"),
},
},
stopTracing: {
request: {},
},
},
});
exports.tracerSpec = tracerSpec;