Source code

Revision control

Copy as Markdown

Other Tools

/* 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 { JSTracer } = ChromeUtils.importESModule(
{ global: "shared" }
);
let lineToTrace;
const fileContents = new Map();
function getFileContent(url) {
let content = fileContents.get(url);
if (content) {
return content;
}
content = readURI(url).split("\n");
fileContents.set(url, content);
return content;
}
function isNestedFrame(frame, topFrame) {
if (frame.older) {
// older will be a Debugger.Frame
while ((frame = frame.older)) {
if (frame == topFrame) {
return true;
}
}
} else if (frame.olderSavedFrame) {
// olderSavedFrame will be a SavedStack object
frame = frame.olderSavedFrame;
const { lineNumber, columnNumber } = topFrame.script.getOffsetMetadata(
top.offset
);
while ((frame = frame.parent || frame.asyncParent)) {
if (
frame.source == topFrame.script.source.url &&
frame.line == lineNumber &&
frame.column == columnNumber
) {
return true;
}
}
}
return false;
}
// Store the top most frame running at `lineToTrace` line.
// We will then log all frames which are children of this top one.
let initialFrame = null;
let previousSourceUrl = null;
function traceFrame({ frame }) {
const { script } = frame;
const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
if (lineToTrace) {
if (lineNumber == lineToTrace) {
// Stop the first tracer started from `exports.start()` which was only waiting for the particular test script line to run
JSTracer.stopTracing();
const { url } = script.source;
const filename = url.substr(url.lastIndexOf("/") + 1);
const line = getFileContent(url)[lineNumber - 1];
logStep(`Start tracing ${filename} @ ${lineNumber} :: ${line}`);
previousSourceUrl = url;
// Restart a new tracer which would go track all the globals and not restrict to the test script.
const tracerOptions = {
// Ensure tracing all globals in this thread
traceAllGlobals: true,
// Ensure tracing each execution within functions (and not only function calls)
traceSteps: true,
};
lineToTrace = null;
JSTracer.startTracing(tracerOptions);
}
return false;
}
// We executed the test line we wanted to trace and now log all frames via a second tracer instance
// First pick up the very first executed frame, so that we can trace all nested frame from this one.
if (!initialFrame) {
initialFrame = frame;
} else if (initialFrame.terminated) {
// If the traced top frame completed its execution, stop tracing.
// Note that terminated will only be true once any possibly asynchronous work of the traced function
// is done executing.
logStep("End of execution");
exports.stop();
return false;
} else if (!initialFrame.onStack) {
// If we are done executing the traced Frame, it will be declared out of the stack.
// By we should keep tracing as, if the traced Frame involves async work, it may be later restored onto the stack.
return false;
} else if (frame != initialFrame && !isNestedFrame(frame, initialFrame)) {
// Then, only log frame which ultimately related to this first frame we picked.
// Because of asynchronous calls and concurrent event loops, we may have in-between frames
// that we ignore which relates to another event loop and another top frame.
//
// Note that the tracer may notify us about the exact same Frame object multiple times.
// (its offset/location will change, but the object will be the same)
return false;
}
const { url } = script.source;
// Print the full source URL each time we start tracing a new source
if (previousSourceUrl && previousSourceUrl !== url) {
logStep("");
logStep(url);
// Log a grey line separator
logStep(`\x1b[2m` + `\u2500`.repeat(url.length) + `\x1b[0m`);
previousSourceUrl = url;
}
const line = getFileContent(url)[lineNumber - 1];
// Grey out the beginning of the line, before frame's column,
// and display an arrow before displaying the rest of the line.
const code =
"\x1b[2m" +
line.substr(0, columnNumber - 1) +
"\x1b[0m" +
"\u21A6 " +
line.substr(columnNumber - 1);
const position = (lineNumber + ":" + columnNumber).padEnd(7);
logStep(`${position} \u007C ${code}`);
// Disable builtin tracer logging
return false;
}
function logStep(message) {
dump(` \x1b[2m[STEP]\x1b[0m ${message}\n`);
}
const tracingListener = {
onTracingFrame: traceFrame,
onTracingFrameStep: traceFrame,
};
exports.start = function (testGlobal, testUrl, line) {
lineToTrace = line;
const tracerOptions = {
global: testGlobal,
// Ensure tracing each execution within functions (and not only function calls)
traceSteps: true,
// Only trace the running test and nothing else
filterFrameSourceUrl: testUrl,
};
JSTracer.startTracing(tracerOptions);
JSTracer.addTracingListener(tracingListener);
};
exports.stop = function () {
JSTracer.stopTracing();
JSTracer.removeTracingListener(tracingListener);
};
function readURI(uri) {
const { NetUtil } = ChromeUtils.importESModule(
{ global: "contextual" }
);
const stream = NetUtil.newChannel({
uri: NetUtil.newURI(uri, "UTF-8"),
loadUsingSystemPrincipal: true,
}).open();
const count = stream.available();
const data = NetUtil.readInputStreamToString(stream, count, {
charset: "UTF-8",
});
stream.close();
return data;
}