Skip to content

Commit

Permalink
Bug 1867599 - Add profiler logging method to JS tracer r=devtools-rev…
Browse files Browse the repository at this point in the history
…iewers,nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D190576
  • Loading branch information
canova committed Dec 4, 2023
1 parent 60bdcaa commit 6f156b8
Show file tree
Hide file tree
Showing 7 changed files with 443 additions and 52 deletions.
46 changes: 46 additions & 0 deletions devtools/client/framework/toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ loader.lazyRequireGetter(
"resource://devtools/client/shared/source-map-loader/index.js",
true
);
loader.lazyRequireGetter(
this,
"openProfilerTab",
"resource://devtools/client/performance-new/shared/browser.js",
true
);
loader.lazyGetter(this, "ProfilerBackground", () => {
return ChromeUtils.import(
"resource://devtools/client/performance-new/shared/background.jsm.js"
);
});

/**
* A "Toolbox" is the component that holds all the tools for one specific
Expand Down Expand Up @@ -656,6 +667,29 @@ Toolbox.prototype = {
}
},

/**
* Called on each new TRACING_STATE resource
*
* @param {Object} resource The TRACING_STATE resource
*/
async _onTracingStateChanged(resource) {
const { profile } = resource;
if (!profile) {
return;
}
const browser = await openProfilerTab();

const profileCaptureResult = {
type: "SUCCESS",
profile,
};
ProfilerBackground.registerProfileCaptureForBrowser(
browser,
profileCaptureResult,
null
);
},

/**
* Be careful, this method is synchronous, but highlightTool, raise, selectTool
* are all async.
Expand Down Expand Up @@ -886,6 +920,15 @@ Toolbox.prototype = {
this.resourceCommand.TYPES.THREAD_STATE,
];

if (
Services.prefs.getBoolPref(
"devtools.debugger.features.javascript-tracing",
false
)
) {
watchedResources.push(this.resourceCommand.TYPES.TRACING_STATE);
}

if (!this.isBrowserToolbox) {
// Independently of watching network event resources for the error count icon,
// we need to start tracking network activity on toolbox open for targets such
Expand Down Expand Up @@ -4701,6 +4744,9 @@ Toolbox.prototype = {
if (resourceType == TYPES.THREAD_STATE) {
this._onThreadStateChanged(resource);
}
if (resourceType == TYPES.TRACING_STATE) {
this._onTracingStateChanged(resource);
}
}

this.setErrorCount(errors);
Expand Down
5 changes: 5 additions & 0 deletions devtools/client/locales/en-US/webconsole.properties
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,11 @@ webconsole.message.commands.startTracingToWebConsole=Started tracing to Web Cons
# Label displayed when :trace command was executed and the JavaScript tracer started to log to stdout.
webconsole.message.commands.startTracingToStdout=Started tracing to stdout

# LOCALIZATION NOTE (webconsole.message.commands.startTracingToProfiler)
# Label displayed when :trace command was executed and the JavaScript tracer will open the profiler showing all the traces,
# but only on stop.
webconsole.message.commands.startTracingToProfiler=Started tracing to the Profiler. The traces will be displayed in the profiler on stop.

# LOCALIZATION NOTE (webconsole.message.commands.stopTracing)
# Label displayed when :trace command was executed and the JavaScript tracer stopped.
webconsole.message.commands.stopTracing=Stopped tracing
Expand Down
4 changes: 4 additions & 0 deletions devtools/client/webconsole/actions/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ function handleHelperResult(response) {
message = l10n.getStr(
"webconsole.message.commands.startTracingToWebConsole"
);
} else if (logMethod == "profiler") {
message = l10n.getStr(
"webconsole.message.commands.startTracingToProfiler"
);
} else {
throw new Error(`Unsupported tracer log method ${logMethod}`);
}
Expand Down
8 changes: 7 additions & 1 deletion devtools/server/actors/resources/tracing-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const {
removeTracingListener,
} = require("resource://devtools/server/tracer/tracer.jsm");

const { LOG_METHODS } = require("resource://devtools/server/actors/tracer.js");

class TracingStateWatcher {
/**
* Start watching for tracing state changes for a given target actor.
Expand Down Expand Up @@ -49,12 +51,16 @@ class TracingStateWatcher {
// When Javascript tracing is enabled or disabled.
onTracingToggled(enabled) {
const tracerActor = this.targetActor.getTargetScopedActor("tracer");
const logMethod = tracerActor?.getLogMethod() | "stdout";
const logMethod = tracerActor?.getLogMethod();
this.onAvailable([
{
resourceType: TRACING_STATE,
enabled,
logMethod,
profile:
logMethod == LOG_METHODS.PROFILER && !enabled
? tracerActor.getProfile()
: undefined,
},
]);
}
Expand Down
146 changes: 95 additions & 51 deletions devtools/server/actors/tracer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ const {
getResourceWatcher,
} = require("resource://devtools/server/actors/resources/index.js");

loader.lazyRequireGetter(
this,
"GeckoProfileCollector",
"resource://devtools/server/actors/utils/gecko-profile-collector.js",
true
);

const LOG_METHODS = {
STDOUT: "stdout",
CONSOLE: "console",
PROFILER: "profiler",
};
exports.LOG_METHODS = LOG_METHODS;
const VALID_LOG_METHODS = Object.values(LOG_METHODS);

const CONSOLE_ARGS_STYLES = [
Expand Down Expand Up @@ -61,6 +70,8 @@ class TracerActor extends Actor {
this.flushConsoleMessages.bind(this),
CONSOLE_THROTTLING_DELAY
);

this.geckoProfileCollector = new GeckoProfileCollector();
}

destroy() {
Expand All @@ -84,39 +95,40 @@ class TracerActor extends Actor {
*/
toggleTracing(options) {
if (!this.tracingListener) {
if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) {
throw new Error(
`Invalid log method '${options.logMethod}'. Only supports: ${VALID_LOG_METHODS}`
);
}
if (options.prefix && typeof options.prefix != "string") {
throw new Error("Invalid prefix, only support string type");
}
this.logMethod = options.logMethod || LOG_METHODS.STDOUT;
this.tracingListener = {
onTracingFrame: this.onTracingFrame.bind(this),
onTracingInfiniteLoop: this.onTracingInfiniteLoop.bind(this),
};
addTracingListener(this.tracingListener);
startTracing({
global: this.targetActor.window || this.targetActor.workerGlobal,
prefix: options.prefix || "",
});
this.#startTracing(options);
return true;
}
this.stopTracing();
return false;
}

startTracing(logMethod = LOG_METHODS.STDOUT) {
this.logMethod = logMethod;
this.#startTracing({ logMethod });
}

#startTracing(options) {
if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) {
throw new Error(
`Invalid log method '${options.logMethod}'. Only supports: ${VALID_LOG_METHODS}`
);
}
if (options.prefix && typeof options.prefix != "string") {
throw new Error("Invalid prefix, only support string type");
}
this.logMethod = options.logMethod || LOG_METHODS.STDOUT;

if (this.logMethod == LOG_METHODS.PROFILER) {
this.geckoProfileCollector.start();
}

this.tracingListener = {
onTracingFrame: this.onTracingFrame.bind(this),
onTracingInfiniteLoop: this.onTracingInfiniteLoop.bind(this),
};
addTracingListener(this.tracingListener);
startTracing({
global: this.targetActor.window || this.targetActor.workerGlobal,
prefix: options.prefix || "",
// Enable receiving the `currentDOMEvent` being passed to `onTracingFrame`
traceDOMEvents: true,
});
Expand All @@ -132,10 +144,28 @@ class TracerActor extends Actor {
this.tracingListener = null;
}

/**
* Queried by THREAD_STATE watcher to send the gecko profiler data
* as part of THREAD STATE "stop" resource.
*
* @return {Object} Gecko profiler profile object.
*/
getProfile() {
const profile = this.geckoProfileCollector.stop();
// We only open the profile if it contains samples, otherwise it can crash the frontend.
if (profile.threads[0].samples.data.length) {
return profile;
}
return null;
}

onTracingInfiniteLoop() {
if (this.logMethod == LOG_METHODS.STDOUT) {
return true;
}
if (this.logMethod == LOG_METHODS.PROFILER) {
this.geckoProfileCollector.stop();
}
const consoleMessageWatcher = getResourceWatcher(
this.targetActor,
TYPES.CONSOLE_MESSAGE
Expand Down Expand Up @@ -210,47 +240,61 @@ 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];
if (this.logMethod == LOG_METHODS.CONSOLE) {
// 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 = [
"—".repeat(depth + 1),
frame.implementation,
"⟶",
formatedDisplayName,
];
// Avoid logging an empty string as console.log would expand it to <empty string>
if (prefix) {
args.unshift(prefix);
}

// Create a message object that fits Console Message Watcher expectations
this.throttledConsoleMessages.push({
arguments: DOMEventArgs,
styles: DOM_EVENT_CONSOLE_ARGS_STYLES,
filename: url,
lineNumber,
columnNumber: columnNumber - columnBase,
arguments: args,
// As we log different number of arguments with/without prefix, use distinct styles
styles: prefix ? CONSOLE_ARGS_STYLES_WITH_PREFIX : CONSOLE_ARGS_STYLES,
level: "logTrace",
chromeContext: this.isChromeContext,
sourceId: script.source.id,
timeStamp: ChromeUtils.dateNow(),
});
this.throttleLogMessages();
} else if (this.logMethod == LOG_METHODS.PROFILER) {
this.geckoProfileCollector.addSample(
{
// formatedDisplayName has a lambda at the beginning, remove it.
name: formatedDisplayName.replace("λ ", ""),
url,
lineNumber,
columnNumber,
category: frame.implementation,
},
depth
);
}

const args = [
"—".repeat(depth + 1),
frame.implementation,
"⟶",
formatedDisplayName,
];
// Avoid logging an empty string as console.log would expand it to <empty string>
if (prefix) {
args.unshift(prefix);
}

// Create a message object that fits Console Message Watcher expectations
this.throttledConsoleMessages.push({
filename: url,
lineNumber,
columnNumber: columnNumber - columnBase,
arguments: args,
// As we log different number of arguments with/without prefix, use distinct styles
styles: prefix ? CONSOLE_ARGS_STYLES_WITH_PREFIX : CONSOLE_ARGS_STYLES,
level: "logTrace",
chromeContext: this.isChromeContext,
sourceId: script.source.id,
timeStamp: ChromeUtils.dateNow(),
});
this.throttleLogMessages();

return false;
}

Expand Down
Loading

0 comments on commit 6f156b8

Please sign in to comment.