Bug 1870710 - [devtools] Optionally trigger the tracer only on next mousedown or keydown. r=devtools-reviewers,fluent-reviewers,flod,nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D196830
This commit is contained in:
Alexandre Poirot 2023-12-20 23:03:06 +00:00
Родитель acd7b3af58
Коммит b2321bf866
16 изменённых файлов: 224 добавлений и 25 удалений

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

@ -240,6 +240,12 @@ export function toggleJavascriptTracingValues() {
};
}
export function toggleJavascriptTracingOnNextInteraction() {
return {
type: "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_INTERACTION",
};
}
export function setHideOrShowIgnoredSources(shouldHide) {
return ({ dispatch, getState }) => {
dispatch({ type: "HIDE_IGNORED_SOURCES", shouldHide });

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

@ -17,6 +17,7 @@ import {
getIsThreadCurrentlyTracing,
getJavascriptTracingLogMethod,
getJavascriptTracingValues,
getJavascriptTracingOnNextInteraction,
} from "../../selectors";
import { formatKeyShortcut } from "../../utils/text";
import actions from "../../actions";
@ -119,6 +120,7 @@ class CommandBar extends Component {
toggleTracing: PropTypes.func.isRequired,
logMethod: PropTypes.string.isRequired,
logValues: PropTypes.bool.isRequired,
traceOnNextInteraction: PropTypes.bool.isRequired,
setJavascriptTracingLogMethod: PropTypes.func.isRequired,
setHideOrShowIgnoredSources: PropTypes.func.isRequired,
toggleSourceMapIgnoreList: PropTypes.func.isRequired,
@ -254,6 +256,15 @@ class CommandBar extends Component {
this.props.toggleJavascriptTracingValues();
},
},
{
id: "debugger-trace-menu-item-next-interaction",
label: L10N.getStr("traceOnNextInteraction"),
type: "checkbox",
checked: this.props.traceOnNextInteraction,
click: () => {
this.props.toggleJavascriptTracingOnNextInteraction();
},
},
];
showMenu(event, items);
},
@ -423,12 +434,15 @@ const mapStateToProps = state => ({
isTracingEnabled: getIsThreadCurrentlyTracing(state, getCurrentThread(state)),
logMethod: getJavascriptTracingLogMethod(state),
logValues: getJavascriptTracingValues(state),
traceOnNextInteraction: getJavascriptTracingOnNextInteraction(state),
});
export default connect(mapStateToProps, {
toggleTracing: actions.toggleTracing,
setJavascriptTracingLogMethod: actions.setJavascriptTracingLogMethod,
toggleJavascriptTracingValues: actions.toggleJavascriptTracingValues,
toggleJavascriptTracingOnNextInteraction:
actions.toggleJavascriptTracingOnNextInteraction,
resume: actions.resume,
stepIn: actions.stepIn,
stepOut: actions.stepOut,

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

@ -37,6 +37,7 @@ export const initialUIState = ({ supportsDebuggerStatementIgnore } = {}) => ({
javascriptEnabled: true,
javascriptTracingLogMethod: prefs.javascriptTracingLogMethod,
javascriptTracingValues: prefs.javascriptTracingValues,
javascriptTracingOnNextInteraction: prefs.javascriptTracingOnNextInteraction,
mutableSearchOptions: prefs.searchOptions || {
[searchKeys.FILE_SEARCH]: {
regexMatch: false,
@ -173,6 +174,16 @@ function update(state = initialUIState(), action) {
};
}
case "TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_INTERACTION": {
prefs.javascriptTracingOnNextInteraction =
!prefs.javascriptTracingOnNextInteraction;
return {
...state,
javascriptTracingOnNextInteraction:
prefs.javascriptTracingOnNextInteraction,
};
}
case "SET_SEARCH_OPTIONS": {
state.mutableSearchOptions[action.searchKey] = {
...state.mutableSearchOptions[action.searchKey],

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

@ -76,6 +76,10 @@ export function getJavascriptTracingValues(state) {
return state.ui.javascriptTracingValues;
}
export function getJavascriptTracingOnNextInteraction(state) {
return state.ui.javascriptTracingOnNextInteraction;
}
export function getSearchOptions(state, searchKey) {
return state.ui.mutableSearchOptions[searchKey];
}

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

@ -46,6 +46,7 @@ if (isNode()) {
pref("devtools.debugger.log-event-breakpoints", false);
pref("devtools.debugger.javascript-tracing-log-method", "console");
pref("devtools.debugger.javascript-tracing-values", false);
pref("devtools.debugger.javascript-tracing-on-next-interaction", false);
pref("devtools.debugger.hide-ignored-sources", false);
pref("devtools.debugger.source-map-ignore-list-enabled", true);
pref("devtools.debugger.features.wasm", true);
@ -106,6 +107,10 @@ export const prefs = new PrefsHelper("devtools", {
"debugger.javascript-tracing-log-method",
],
javascriptTracingValues: ["Bool", "debugger.javascript-tracing-values"],
javascriptTracingOnNextInteraction: [
"Bool",
"debugger.javascript-tracing-on-next-interaction",
],
hideIgnoredSources: ["Bool", "debugger.hide-ignored-sources"],
sourceMapIgnoreListEnabled: [
"Bool",

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

@ -325,7 +325,7 @@ add_task(async function testTracingValues() {
"data:text/html," + encodeURIComponent(`<script>${jsCode}</script>`)
);
await openContextMenuInDebugger(dbg, "trace", 4);
await openContextMenuInDebugger(dbg, "trace");
const toggled = waitForDispatch(
dbg.store,
"TOGGLE_JAVASCRIPT_TRACING_VALUES"
@ -357,3 +357,67 @@ add_task(async function testTracingValues() {
// Reset the log values setting
Services.prefs.clearUserPref("devtools.debugger.javascript-tracing-values");
});
add_task(async function testTracingOnNextInteraction() {
await pushPref("devtools.debugger.features.javascript-tracing", true);
// Cover tracing only on next user interaction
const jsCode = `function foo() {}; window.addEventListener("mousedown", function onmousedown(){}, { capture: true }); window.onclick = function onclick() {};`;
const dbg = await initDebuggerWithAbsoluteURL(
"data:text/html," +
encodeURIComponent(`<script>${jsCode}</script><body></body>`)
);
await openContextMenuInDebugger(dbg, "trace");
const toggled = waitForDispatch(
dbg.store,
"TOGGLE_JAVASCRIPT_TRACING_ON_NEXT_INTERACTION"
);
selectContextMenuItem(dbg, `#debugger-trace-menu-item-next-interaction`);
await toggled;
ok(true, "Toggled the trace on next interaction");
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);
});
invokeInTab("foo");
// Let a change to have the tracer to regress and log foo call
await wait(500);
is(
(await findConsoleMessages(dbg.toolbox, "λ foo")).length,
0,
"The tracer did not log the function call before trigerring the click event"
);
await BrowserTestUtils.synthesizeMouseAtCenter(
"body",
{},
gBrowser.selectedBrowser
);
await hasConsoleMessage(dbg, "λ onmousedown");
await hasConsoleMessage(dbg, "λ onclick");
is(
(await findConsoleMessages(dbg.toolbox, "λ foo")).length,
0,
"Even after the click, the code called before the click is still not logged"
);
// But if we call this code again, now it should be logged
invokeInTab("foo");
await hasConsoleMessage(dbg, "λ foo");
ok(true, "foo was traced as expected");
// Reset the trace on next interaction setting
Services.prefs.clearUserPref(
"devtools.debugger.javascript-tracing-on-next-interaction"
);
});

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

@ -148,6 +148,11 @@ traceInStdout=Trace in the stdout
# as well as returned values (only for JS function calls, but not native function calls)
traceValues=Log function arguments and returned values
# LOCALIZATION NOTE (traceOnNextLoad): 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 automatically start the tracing on next user interaction (mousedown/keydown)
traceOnNextInteraction=Trace only on next user interaction (mousedown/keydown)
# LOCALIZATION NOTE (resumeButtonTooltip): The label that is displayed on the pause
# button when the debugger is in a paused state.
resumeButtonTooltip=Resume %S

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

@ -12,6 +12,7 @@ const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html>
<h1>Testing trace command</h1>
<script>
function main() {}
function someNoise() {}
</script>
</div>
<div><p></p></div>
@ -43,11 +44,19 @@ add_task(async function () {
// Instead the frontend log a message as a console API message.
msg = await evaluateExpressionInConsole(
hud,
":trace --logMethod console --prefix foo --values",
":trace --logMethod console --prefix foo --values --on-next-interaction",
"console-api"
);
is(msg.textContent.trim(), "Started tracing to Web Console");
info("Trigger some code before the user interaction");
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
content.wrappedJSObject.someNoise();
});
info("Simulate a user interaction by trigerring a key event on the page");
await BrowserTestUtils.synthesizeKey("a", {}, gBrowser.selectedBrowser);
info("Trigger some code to log some traces");
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
content.wrappedJSObject.main("arg", 2);
@ -57,6 +66,19 @@ add_task(async function () {
await waitFor(
() => !!findTracerMessages(hud, `foo: interpreter⟶λ main("arg", 2)`).length
);
is(
findTracerMessages(hud, `someNoise`).length,
0,
"The code running before the key press should not be traced"
);
// But now that the tracer is active, we will be able to log this call to someNoise
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
content.wrappedJSObject.someNoise();
});
await waitFor(
() => !!findTracerMessages(hud, `foo: interpreter⟶λ someNoise()`).length
);
info("Test toggling the tracer OFF");
msg = await evaluateExpressionInConsole(hud, ":trace", "console-api");

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

@ -132,6 +132,8 @@ class TracerActor extends Actor {
traceDOMEvents: true,
// Enable tracing function arguments as well as returned values
traceValues: !!options.traceValues,
// Enable tracing only on next user interaction
traceOnNextInteraction: !!options.traceOnNextInteraction,
});
}

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

@ -0,0 +1,19 @@
# 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/.
# These strings aren't translated and are meant to be used for experimental commands
# which may frequently update their documentations
# Usage string for :trace command
webconsole-commands-usage-trace3 =
:trace
Toggles the JavaScript tracer
It supports the following arguments:
--logMethod to be set to console for logging to the web console (the default), or stdout for logging to the standard output,
--values Optional flag to be passed to log function call arguments as well as returned values (when returned frames are enabled).
--on-next-interaction Optional flag, when set, the tracer will only start on next mousedown or keydown event.
--prefix Optional string which will be logged in front of all the trace logs,
--help or --usage to show this message.

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

@ -12,11 +12,17 @@ loader.lazyRequireGetter(
);
loader.lazyGetter(this, "l10n", () => {
return new Localization(["devtools/shared/webconsole-commands.ftl"], true);
return new Localization(
[
"devtools/shared/webconsole-commands.ftl",
"devtools/server/actors/webconsole/commands/experimental-commands.ftl",
],
true
);
});
const USAGE_STRING_MAPPING = {
block: "webconsole-commands-usage-block",
trace: "webconsole-commands-usage-trace2",
trace: "webconsole-commands-usage-trace3",
unblock: "webconsole-commands-usage-unblock",
};
@ -872,6 +878,7 @@ WebConsoleCommandsManager.register({
logMethod,
prefix: args.prefix || null,
traceValues: !!args.values,
traceOnNextInteraction: args["on-next-interaction"] || null,
});
owner.helperResult = {
@ -880,5 +887,5 @@ WebConsoleCommandsManager.register({
logMethod,
};
},
validArguments: ["logMethod", "prefix", "values"],
validArguments: ["logMethod", "prefix", "values", "on-next-interaction"],
});

8
devtools/server/jar.mn Normal file
Просмотреть файл

@ -0,0 +1,8 @@
#filter substitution
# 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/.
[localization] @AB_CD@.jar:
# This is a workaround to ship a fluent file not visible to localizers for experimental features.
devtools/server/actors/webconsole/commands/experimental-commands.ftl (actors/webconsole/commands/experimental-commands.ftl)

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

@ -15,6 +15,8 @@ DIRS += [
"tracer",
]
JAR_MANIFESTS += ["jar.mn"]
if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]

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

@ -92,6 +92,9 @@ const customLazy = {
* @param {Boolean} options.traceValues
* Optional setting to enable tracing all function call values as well,
* as returned values (when we do log returned frames).
* @param {Boolean} options.traceOnNextInteraction
* Optional setting to enable when the tracing should only start when the
* use starts interacting with the page. i.e. on next keydown or mousedown.
*/
class JavaScriptTracer {
constructor(options) {
@ -120,15 +123,40 @@ class JavaScriptTracer {
: dump;
}
this.traceDOMEvents = !!options.traceDOMEvents;
this.traceValues = !!options.traceValues;
if (options.traceOnNextInteraction) {
this.abortController = new AbortController();
const listener = () => {
this.abortController.abort();
// Avoid tracing if the users asked to stop tracing.
if (this.dbg) {
this.#startTracing();
}
};
const eventOptions = {
signal: this.abortController.signal,
capture: true,
};
// Register the event listener on the Chrome Event Handler in order to receive the event first.
const eventHandler = this.tracedGlobal.docShell.chromeEventHandler;
eventHandler.addEventListener("mousedown", listener, eventOptions);
eventHandler.addEventListener("keydown", listener, eventOptions);
} else {
this.#startTracing();
}
// In any case, we consider the tracing as started
this.notifyToggle(true);
}
#startTracing() {
this.dbg.onEnterFrame = this.onEnterFrame;
this.traceDOMEvents = !!options.traceDOMEvents;
if (this.traceDOMEvents) {
this.startTracingDOMEvents();
}
this.traceValues = !!options.traceValues;
this.notifyToggle(true);
}
startTracingDOMEvents() {
@ -141,9 +169,11 @@ class JavaScriptTracer {
}
stopTracingDOMEvents() {
this.debuggerNotificationObserver.removeListener(this.eventListener);
this.debuggerNotificationObserver.disconnect(this.tracedGlobal);
this.debuggerNotificationObserver = null;
if (this.debuggerNotificationObserver) {
this.debuggerNotificationObserver.removeListener(this.eventListener);
this.debuggerNotificationObserver.disconnect(this.tracedGlobal);
this.debuggerNotificationObserver = null;
}
this.currentDOMEvent = null;
}
@ -186,6 +216,10 @@ class JavaScriptTracer {
this.dbg = null;
this.depth = 0;
this.options = null;
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
if (this.traceDOMEvents) {
this.stopTracingDOMEvents();

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

@ -38,6 +38,11 @@ class TracerCommand {
false
);
const traceOnNextInteraction = Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-on-next-interaction",
false
);
const targets = this.#targetCommand.getAllTargets(
this.#targetCommand.ALL_TYPES
);
@ -51,7 +56,10 @@ class TracerCommand {
}
if (this.#isTracing) {
return tracerFront.startTracing(logMethod, { traceValues });
return tracerFront.startTracing(logMethod, {
traceValues,
traceOnNextInteraction,
});
}
return tracerFront.stopTracing();
})

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

@ -22,15 +22,3 @@ webconsole-commands-usage-unblock =
Stop blocking network requests
It accepts only one argument, the exact same string previously passed to :block.
# Usage string for :trace command
webconsole-commands-usage-trace2 =
:trace
Toggles the JavaScript tracer
It supports the following arguments:
--logMethod to be set to console for logging to the web console (the default), or stdout for logging to the standard output,
--values Optional flag to be passed to log function call arguments as well as returned values (when returned frames are enabled).
--prefix Optional string which will be logged in front of all the trace logs,
--help or --usage to show this message.