442 lines
14 KiB
JavaScript
442 lines
14 KiB
JavaScript
/* 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 { throttle } = require("resource://devtools/shared/throttle.js");
|
|
|
|
const { makeDebuggeeValue } = require("devtools/server/actors/object/utils");
|
|
|
|
const {
|
|
TYPES,
|
|
getResourceWatcher,
|
|
} = require("resource://devtools/server/actors/resources/index.js");
|
|
const { JSTRACER_TRACE } = TYPES;
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(
|
|
lazy,
|
|
{
|
|
JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs",
|
|
},
|
|
{ global: "contextual" }
|
|
);
|
|
|
|
const {
|
|
getActorIdForInternalSourceId,
|
|
} = require("resource://devtools/server/actors/utils/dbg-source.js");
|
|
|
|
const THROTTLING_DELAY = 250;
|
|
|
|
class ResourcesTracingListener {
|
|
constructor({ targetActor, traceValues, traceActor }) {
|
|
this.targetActor = targetActor;
|
|
this.traceValues = traceValues;
|
|
this.sourcesManager = targetActor.sourcesManager;
|
|
this.traceActor = traceActor;
|
|
|
|
// On workers, we don't have access to setTimeout and can't have throttling
|
|
this.throttleEmitTraces = isWorker
|
|
? this.flushTraces.bind(this)
|
|
: throttle(this.flushTraces.bind(this), THROTTLING_DELAY);
|
|
}
|
|
|
|
// Collect pending data to be sent to the client in various arrays,
|
|
// each focusing on particular data type.
|
|
// All these arrays contains arrays as elements.
|
|
#throttledTraces = [];
|
|
|
|
// Index of the next collected frame
|
|
#frameIndex = 0;
|
|
// Three level of Maps, ultimately storing frame indexes.
|
|
// The first level of Map is keyed by source ID,
|
|
// the second by line number,
|
|
// the last by column number.
|
|
// Frame objects are sent to the client and not being held in memory,
|
|
// we only store their related indexes which are put in trace arrays.
|
|
#frameMap = new Map();
|
|
|
|
/**
|
|
* Called when the tracer stops recording JS executions.
|
|
*/
|
|
stop() {
|
|
this.#frameIndex = 0;
|
|
this.#frameMap.clear();
|
|
}
|
|
|
|
/**
|
|
* This method is throttled and will notify all pending traces to be logged in the console
|
|
* via the console message watcher.
|
|
*/
|
|
flushTraces() {
|
|
const traceWatcher = getResourceWatcher(this.targetActor, JSTRACER_TRACE);
|
|
// Ignore the request if the frontend isn't listening to traces for that target.
|
|
if (!traceWatcher) {
|
|
return;
|
|
}
|
|
const traces = this.#throttledTraces;
|
|
this.#throttledTraces = [];
|
|
|
|
traceWatcher.emitTraces(traces);
|
|
}
|
|
|
|
/**
|
|
* Be notified by the underlying JavaScriptTracer class
|
|
* in case it stops by itself, instead of being stopped when the Actor's stopTracing
|
|
* method is called by the user.
|
|
*
|
|
* @param {Boolean} enabled
|
|
* True if the tracer starts tracing, false it it stops.
|
|
* @return {Boolean}
|
|
* Return true, if the JavaScriptTracer should log a message to stdout.
|
|
*/
|
|
onTracingToggled(enabled) {
|
|
if (!enabled) {
|
|
this.traceActor.stopTracing();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
const consoleMessageWatcher = getResourceWatcher(
|
|
this.targetActor,
|
|
TYPES.CONSOLE_MESSAGE
|
|
);
|
|
if (consoleMessageWatcher) {
|
|
consoleMessageWatcher.emitMessages([
|
|
{
|
|
arguments: [lazy.JSTracer.NEXT_INTERACTION_MESSAGE],
|
|
styles: [],
|
|
level: "jstracer",
|
|
chromeContext: false,
|
|
timeStamp: ChromeUtils.dateNow(),
|
|
},
|
|
]);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called by JavaScriptTracer class when a new mutation happened on any DOM Element.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} options.depth
|
|
* Represents the depth of the frame in the call stack.
|
|
* @param {String} options.prefix
|
|
* A string to be displayed as a prefix of any logged frame.
|
|
* @param {nsIStackFrame} options.caller
|
|
* The JS Callsite which caused this mutation.
|
|
* @param {String} options.type
|
|
* Type of DOM Mutation:
|
|
* - "add": Node being added,
|
|
* - "attributes": Node whose attributes changed,
|
|
* - "remove": Node being removed,
|
|
* @param {DOMNode} options.element
|
|
* The DOM Node related to the current mutation.
|
|
* @return {Boolean}
|
|
* Return true, if the JavaScriptTracer should log a message to stdout.
|
|
*/
|
|
onTracingDOMMutation({ depth, prefix, type, caller, element }) {
|
|
const dbgObj = makeDebuggeeValue(this.targetActor, element);
|
|
const frameIndex = this.#getFrameIndex(
|
|
null,
|
|
null,
|
|
caller
|
|
? getActorIdForInternalSourceId(this.targetActor, caller.sourceId)
|
|
: null,
|
|
caller?.lineNumber,
|
|
caller?.columnNumber,
|
|
caller?.filename
|
|
);
|
|
this.#throttledTraces.push([
|
|
"dom-mutation",
|
|
prefix,
|
|
frameIndex,
|
|
ChromeUtils.dateNow(),
|
|
depth,
|
|
type,
|
|
this.traceActor.createValueGrip(dbgObj),
|
|
]);
|
|
this.throttleEmitTraces();
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called by JavaScriptTracer class on each step of a function call.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Debugger.Frame} options.frame
|
|
* A descriptor object for the JavaScript frame.
|
|
* @param {Number} options.depth
|
|
* Represents the depth of the frame in the call stack.
|
|
* @param {String} options.prefix
|
|
* A string to be displayed as a prefix of any logged frame.
|
|
* @return {Boolean}
|
|
* Return true, if the JavaScriptTracer should log the step to stdout.
|
|
*/
|
|
onTracingFrameStep({ frame, depth, prefix }) {
|
|
const { script } = frame;
|
|
const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
|
|
const url = script.source.url;
|
|
|
|
// NOTE: Debugger.Script.prototype.getOffsetMetadata returns
|
|
// columnNumber in 1-based.
|
|
// Convert to 0-based, while keeping the wasm's column (1) as is.
|
|
// (bug 1863878)
|
|
const columnBase = script.format === "wasm" ? 0 : 1;
|
|
const column = columnNumber - columnBase;
|
|
|
|
// Ignore blackboxed sources
|
|
if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
|
|
return false;
|
|
}
|
|
|
|
const frameIndex = this.#getFrameIndex(
|
|
frame.implementation,
|
|
null,
|
|
getActorIdForInternalSourceId(this.targetActor, script.source.id),
|
|
lineNumber,
|
|
column,
|
|
url
|
|
);
|
|
this.#throttledTraces.push([
|
|
"step",
|
|
prefix,
|
|
frameIndex,
|
|
ChromeUtils.dateNow(),
|
|
depth,
|
|
null,
|
|
]);
|
|
this.throttleEmitTraces();
|
|
|
|
return false;
|
|
}
|
|
|
|
#getFrameIndex(implementation, name, sourceId, line, column, url) {
|
|
let perSourceMap = this.#frameMap.get(sourceId);
|
|
if (!perSourceMap) {
|
|
perSourceMap = new Map();
|
|
this.#frameMap.set(sourceId, perSourceMap);
|
|
}
|
|
let perLineMap = perSourceMap.get(line);
|
|
if (!perLineMap) {
|
|
perLineMap = new Map();
|
|
perSourceMap.set(line, perLineMap);
|
|
}
|
|
let frameIndex = perLineMap.get(column);
|
|
|
|
if (frameIndex == undefined) {
|
|
frameIndex = this.#frameIndex++;
|
|
|
|
// Remember updating TRACER_FIELDS_INDEXES when modifying the following array:
|
|
const frameArray = [
|
|
"frame",
|
|
implementation,
|
|
name,
|
|
sourceId,
|
|
line,
|
|
column,
|
|
url,
|
|
];
|
|
|
|
perLineMap.set(column, frameIndex);
|
|
this.#throttledTraces.push(frameArray);
|
|
}
|
|
return frameIndex;
|
|
}
|
|
|
|
/**
|
|
* Called by JavaScriptTracer class when a new JavaScript frame is executed.
|
|
*
|
|
* @param {Debugger.Frame} frame
|
|
* A descriptor object for the JavaScript frame.
|
|
* @param {Number} depth
|
|
* Represents the depth of the frame in the call stack.
|
|
* @param {String} formatedDisplayName
|
|
* 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,
|
|
currentDOMEvent,
|
|
}) {
|
|
const { script } = frame;
|
|
const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
|
|
const url = script.source.url;
|
|
|
|
// NOTE: Debugger.Script.prototype.getOffsetMetadata returns
|
|
// columnNumber in 1-based.
|
|
// Convert to 0-based, while keeping the wasm's column (1) as is.
|
|
// (bug 1863878)
|
|
const columnBase = script.format === "wasm" ? 0 : 1;
|
|
const column = columnNumber - columnBase;
|
|
|
|
// Ignore blackboxed sources
|
|
if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
|
|
return false;
|
|
}
|
|
|
|
// 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) {
|
|
// Create a JSTRACER_TRACE resource with a slightly different shape
|
|
this.#throttledTraces.push([
|
|
"event",
|
|
prefix,
|
|
null,
|
|
ChromeUtils.dateNow(),
|
|
// Events are parent of any subsequent JS call, which has a 0 depth.
|
|
-1,
|
|
currentDOMEvent,
|
|
]);
|
|
}
|
|
|
|
let args = undefined,
|
|
argNames = undefined;
|
|
// Log arguments, but only when this feature is enabled as it introduce
|
|
// some significant overhead in perf as well as memory as it may hold the objects in memory.
|
|
// Also prevent trying to log function call arguments if we aren't logging a frame
|
|
// with arguments (e.g. Debugger evaluation frames, when executing from the console)
|
|
if (this.traceValues && frame.arguments) {
|
|
args = [];
|
|
for (let arg of frame.arguments) {
|
|
// Debugger.Frame.arguments contains either a Debugger.Object or primitive object
|
|
if (arg?.unsafeDereference) {
|
|
arg = arg.unsafeDereference();
|
|
}
|
|
// Instantiate a object actor so that the tools can easily inspect these objects
|
|
const dbgObj = makeDebuggeeValue(this.targetActor, arg);
|
|
args.push(this.traceActor.createValueGrip(dbgObj));
|
|
}
|
|
argNames = frame.callee.script.parameterNames;
|
|
}
|
|
|
|
// In order for getActorIdForInternalSourceId to work reliably,
|
|
// we have to ensure creating a source actor for that source.
|
|
// It happens on Google Docs that some evaled sources aren't registered?
|
|
this.sourcesManager.getOrCreateSourceActor(script.source);
|
|
|
|
const frameIndex = this.#getFrameIndex(
|
|
frame.implementation,
|
|
formatedDisplayName,
|
|
getActorIdForInternalSourceId(this.targetActor, script.source.id),
|
|
lineNumber,
|
|
column,
|
|
url
|
|
);
|
|
this.#throttledTraces.push([
|
|
"enter",
|
|
prefix,
|
|
frameIndex,
|
|
ChromeUtils.dateNow(),
|
|
depth,
|
|
args,
|
|
argNames,
|
|
]);
|
|
this.throttleEmitTraces();
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called by JavaScriptTracer class when a JavaScript frame exits (i.e. a function returns or throw).
|
|
*
|
|
* @param {Object} options
|
|
* @param {Number} options.frameId
|
|
* Unique identifier for the current frame.
|
|
* This should match a frame notified via onTracingFrame.
|
|
* @param {Debugger.Frame} options.frame
|
|
* A descriptor object for the JavaScript frame.
|
|
* @param {Number} options.depth
|
|
* Represents the depth of the frame in the call stack.
|
|
* @param {String} options.formatedDisplayName
|
|
* A human readable name for the current frame.
|
|
* @param {String} options.prefix
|
|
* A string to be displayed as a prefix of any logged frame.
|
|
* @param {String} options.why
|
|
* A string to explain why the function stopped.
|
|
* See tracer.sys.mjs's FRAME_EXIT_REASONS.
|
|
* @param {Debugger.Object|primitive} options.rv
|
|
* The returned value. It can be the returned value, or the thrown exception.
|
|
* It is either a primitive object, otherwise it is a Debugger.Object for any other JS Object type.
|
|
* @return {Boolean}
|
|
* Return true, if the JavaScriptTracer should log the frame to stdout.
|
|
*/
|
|
onTracingFrameExit({
|
|
frameId,
|
|
frame,
|
|
depth,
|
|
formatedDisplayName,
|
|
prefix,
|
|
why,
|
|
rv,
|
|
}) {
|
|
const { script } = frame;
|
|
const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
|
|
const url = script.source.url;
|
|
|
|
// NOTE: Debugger.Script.prototype.getOffsetMetadata returns
|
|
// columnNumber in 1-based.
|
|
// Convert to 0-based, while keeping the wasm's column (1) as is.
|
|
// (bug 1863878)
|
|
const columnBase = script.format === "wasm" ? 0 : 1;
|
|
const column = columnNumber - columnBase;
|
|
|
|
// Ignore blackboxed sources
|
|
if (this.sourcesManager.isBlackBoxed(url, lineNumber, column)) {
|
|
return false;
|
|
}
|
|
|
|
let returnedValue = undefined;
|
|
// Log arguments, but only when this feature is enabled as it introduce
|
|
// some significant overhead in perf as well as memory as it may hold the objects in memory.
|
|
if (this.traceValues) {
|
|
// Debugger.Frame.arguments contains either a Debugger.Object or primitive object
|
|
if (rv?.unsafeDereference) {
|
|
rv = rv.unsafeDereference();
|
|
}
|
|
// Instantiate a object actor so that the tools can easily inspect these objects
|
|
const dbgObj = makeDebuggeeValue(this.targetActor, rv);
|
|
returnedValue = this.traceActor.createValueGrip(dbgObj);
|
|
}
|
|
|
|
const frameIndex = this.#getFrameIndex(
|
|
frame.implementation,
|
|
formatedDisplayName,
|
|
getActorIdForInternalSourceId(this.targetActor, script.source.id),
|
|
lineNumber,
|
|
column,
|
|
url
|
|
);
|
|
this.#throttledTraces.push([
|
|
"exit",
|
|
prefix,
|
|
frameIndex,
|
|
ChromeUtils.dateNow(),
|
|
depth,
|
|
frameId,
|
|
returnedValue,
|
|
why,
|
|
]);
|
|
this.throttleEmitTraces();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
exports.ResourcesTracingListener = ResourcesTracingListener;
|