/* 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/. */ /* 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} 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} actualStackFrames - As generated by * inflatedStackFrames. * @param {Array} 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; } }