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
/* import-globals-from ../shared-head.js */
// This Services declaration may shadow another from head.js, so define it as
// a var rather than a const.
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
// Load the shared head
const sharedHead = do_get_file("shared-head.js", false);
if (!sharedHead) {
throw new Error("Could not load the shared head.");
}
Services.scriptloader.loadSubScript(
Services.io.newFileURI(sharedHead).spec,
this
);
/**
* This function takes a thread, and a sample tuple from the "data" array, and
* inflates the frame to be an array of strings.
*
* @param {Object} thread - The thread from the profile.
* @param {Array} sample - The tuple from the thread.samples.data array.
* @returns {Array<string>} An array of function names.
*/
function getInflatedStackLocations(thread, sample) {
let stackTable = thread.stackTable;
let frameTable = thread.frameTable;
let stringTable = thread.stringTable;
let SAMPLE_STACK_SLOT = thread.samples.schema.stack;
let STACK_PREFIX_SLOT = stackTable.schema.prefix;
let STACK_FRAME_SLOT = stackTable.schema.frame;
let FRAME_LOCATION_SLOT = frameTable.schema.location;
// Build the stack from the raw data and accumulate the locations in
// an array.
let stackIndex = sample[SAMPLE_STACK_SLOT];
let locations = [];
while (stackIndex !== null) {
let stackEntry = stackTable.data[stackIndex];
let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
stackIndex = stackEntry[STACK_PREFIX_SLOT];
}
// The profiler tree is inverted, so reverse the array.
return locations.reverse();
}
/**
* This utility matches up stacks to see if they contain a certain sequence of
* stack frames. A correctly functioning profiler will have a certain sequence
* of stacks, but we can't always determine exactly which stacks will show up
* due to implementation changes, as well as memory addresses being arbitrary to
* that particular build.
*
* This function triggers a test failure with a nice debug message when it
* fails.
*
* @param {Array<string>} actualStackFrames - As generated by
* inflatedStackFrames.
* @param {Array<string | RegExp>} expectedStackFrames - Matches a subset of
* actualStackFrames
*/
function expectStackToContain(
actualStackFrames,
expectedStackFrames,
message = "The actual stack and expected stack do not match."
) {
// Log the stacks that are being passed to this assertion, as it could be
// useful for when these tests fail.
console.log("Actual stack: ", actualStackFrames);
console.log(
"Expected to contain: ",
expectedStackFrames.map(s => s.toString())
);
let actualIndex = 0;
// Start walking the expected stack and look for matches.
for (
let expectedIndex = 0;
expectedIndex < expectedStackFrames.length;
expectedIndex++
) {
const expectedStackFrame = expectedStackFrames[expectedIndex];
while (true) {
// Make sure that we haven't run out of actual stack frames.
if (actualIndex >= actualStackFrames.length) {
info(`Could not find a match for: "${expectedStackFrame.toString()}"`);
Assert.ok(false, message);
}
const actualStackFrame = actualStackFrames[actualIndex];
actualIndex++;
const itMatches =
typeof expectedStackFrame === "string"
? expectedStackFrame === actualStackFrame
: actualStackFrame.match(expectedStackFrame);
if (itMatches) {
// We found a match, break out of this loop.
break;
}
// Keep on looping looking for a match.
}
}
Assert.ok(true, message);
}
/**
* @param {Thread} thread
* @param {string} filename - The filename used to trigger FileIO.
* @returns {InflatedMarkers[]}
*/
function getInflatedFileIOMarkers(thread, filename) {
const markers = getInflatedMarkerData(thread);
return markers.filter(
marker =>
marker.data?.type === "FileIO" &&
marker.data?.filename?.endsWith(filename)
);
}
/**
* Checks properties common to all FileIO markers.
*
* @param {InflatedMarkers[]} markers
* @param {string} filename
*/
function checkInflatedFileIOMarkers(markers, filename) {
greater(markers.length, 0, "Found some markers");
// See IOInterposeObserver::Observation::ObservedOperationString
const validOperations = new Set([
"write",
"fsync",
"close",
"stat",
"create/open",
"read",
]);
const validSources = new Set(["PoisonIOInterposer", "NSPRIOInterposer"]);
for (const marker of markers) {
try {
ok(
marker.name.startsWith("FileIO"),
"Has a marker.name that starts with FileIO"
);
equal(marker.data.type, "FileIO", "Has a marker.data.type");
ok(isIntervalMarker(marker), "All FileIO markers are interval markers");
ok(
validOperations.has(marker.data.operation),
`The markers have a known operation - "${marker.data.operation}"`
);
ok(
validSources.has(marker.data.source),
`The FileIO marker has a known source "${marker.data.source}"`
);
ok(marker.data.filename.endsWith(filename));
ok(Boolean(marker.data.stack), "A stack was collected");
} catch (error) {
console.error("Failing inflated FileIO marker:", marker);
throw error;
}
}
}
/**
* Do deep equality checks for schema, but then surface nice errors for a user to know
* what to do if the check fails.
*/
function checkSchema(actual, expected) {
const schemaName = expected.name;
info(`Checking marker schema for "${schemaName}"`);
try {
ok(
actual,
`Schema was found for "${schemaName}". See the test output for more information.`
);
// Check individual properties to surface easier to debug errors.
deepEqual(
expected.display,
actual.display,
`The "display" property for ${schemaName} schema matches. See the test output for more information.`
);
if (expected.data) {
ok(actual.data, `Schema was found for "${schemaName}"`);
for (const expectedDatum of expected.data) {
const actualDatum = actual.data.find(d => d.key === expectedDatum.key);
deepEqual(
expectedDatum,
actualDatum,
`The "${schemaName}" field "${expectedDatum.key}" matches expectations. See the test output for more information.`
);
}
equal(
expected.data.length,
actual.data.length,
"The expected and actual data have the same number of items"
);
}
// Finally do a true deep equal.
deepEqual(expected, actual, "The entire schema is deepEqual");
} catch (error) {
// The test results are not very human readable. This is a bit of a hacky
// solution to make it more readable.
dump("-----------------------------------------------------\n");
dump("The expected marker schema:\n");
dump("-----------------------------------------------------\n");
dump(JSON.stringify(expected, null, 2));
dump("\n");
dump("-----------------------------------------------------\n");
dump("The actual marker schema:\n");
dump("-----------------------------------------------------\n");
dump(JSON.stringify(actual, null, 2));
dump("\n");
dump("-----------------------------------------------------\n");
dump("A marker schema was not equal to expectations. If you\n");
dump("are modifying the schema, then please copy and paste\n");
dump("the new schema into this test.\n");
dump("-----------------------------------------------------\n");
dump("Copy this: " + JSON.stringify(actual));
dump("\n");
dump("-----------------------------------------------------\n");
throw error;
}
}