summaryrefslogtreecommitdiffstats
path: root/tools/profiler/tests/xpcshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /tools/profiler/tests/xpcshell
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--tools/profiler/tests/xpcshell/head.js244
-rw-r--r--tools/profiler/tests/xpcshell/test_active_configuration.js115
-rw-r--r--tools/profiler/tests/xpcshell/test_addProfilerMarker.js221
-rw-r--r--tools/profiler/tests/xpcshell/test_asm.js76
-rw-r--r--tools/profiler/tests/xpcshell/test_assertion_helper.js162
-rw-r--r--tools/profiler/tests/xpcshell/test_enterjit_osr.js52
-rw-r--r--tools/profiler/tests/xpcshell/test_enterjit_osr_disabling.js14
-rw-r--r--tools/profiler/tests/xpcshell/test_enterjit_osr_enabling.js14
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_fileioall.js159
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_java.js31
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_js.js63
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_mainthreadio.js122
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_nativeallocations.js158
-rw-r--r--tools/profiler/tests/xpcshell/test_feature_stackwalking.js48
-rw-r--r--tools/profiler/tests/xpcshell/test_get_features.js8
-rw-r--r--tools/profiler/tests/xpcshell/test_merged_stacks.js74
-rw-r--r--tools/profiler/tests/xpcshell/test_pause.js126
-rw-r--r--tools/profiler/tests/xpcshell/test_responsiveness.js50
-rw-r--r--tools/profiler/tests/xpcshell/test_run.js37
-rw-r--r--tools/profiler/tests/xpcshell/test_shared_library.js21
-rw-r--r--tools/profiler/tests/xpcshell/test_start.js21
-rw-r--r--tools/profiler/tests/xpcshell/xpcshell.ini72
22 files changed, 1888 insertions, 0 deletions
diff --git a/tools/profiler/tests/xpcshell/head.js b/tools/profiler/tests/xpcshell/head.js
new file mode 100644
index 0000000000..ce87b32fd5
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/head.js
@@ -0,0 +1,244 @@
+/* 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<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;
+ }
+}
diff --git a/tools/profiler/tests/xpcshell/test_active_configuration.js b/tools/profiler/tests/xpcshell/test_active_configuration.js
new file mode 100644
index 0000000000..c4336f3f32
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_active_configuration.js
@@ -0,0 +1,115 @@
+/* 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/. */
+
+add_task(async () => {
+ info(
+ "Checking that the profiler can fetch the information about the active " +
+ "configuration that is being used to power the profiler."
+ );
+
+ equal(
+ Services.profiler.activeConfiguration,
+ null,
+ "When the profile is off, there is no active configuration."
+ );
+
+ {
+ info("Start the profiler.");
+ const entries = 10000;
+ const interval = 1;
+ const threads = ["GeckoMain"];
+ const features = ["js"];
+ const activeTabID = 123;
+ await Services.profiler.StartProfiler(
+ entries,
+ interval,
+ features,
+ threads,
+ activeTabID
+ );
+
+ info("Generate the activeConfiguration.");
+ const { activeConfiguration } = Services.profiler;
+ const expectedConfiguration = {
+ interval,
+ threads,
+ features,
+ activeTabID,
+ // The buffer is created as a power of two that can fit all of the entires
+ // into it. If the ratio of entries to buffer size ever changes, this setting
+ // will need to be updated.
+ capacity: Math.pow(2, 14),
+ };
+
+ deepEqual(
+ activeConfiguration,
+ expectedConfiguration,
+ "The active configuration matches configuration given."
+ );
+
+ info("Get the profile.");
+ const profile = Services.profiler.getProfileData();
+ deepEqual(
+ profile.meta.configuration,
+ expectedConfiguration,
+ "The configuration also matches on the profile meta object."
+ );
+ }
+
+ {
+ const entries = 20000;
+ const interval = 0.5;
+ const threads = ["GeckoMain", "DOM Worker"];
+ const features = [];
+ const activeTabID = 111;
+ const duration = 20;
+
+ info("Restart the profiler with a new configuration.");
+ await Services.profiler.StartProfiler(
+ entries,
+ interval,
+ features,
+ threads,
+ activeTabID,
+ // Also start it with duration, this property is optional.
+ duration
+ );
+
+ info("Generate the activeConfiguration.");
+ const { activeConfiguration } = Services.profiler;
+ const expectedConfiguration = {
+ interval,
+ threads,
+ features,
+ activeTabID,
+ duration,
+ // The buffer is created as a power of two that can fit all of the entires
+ // into it. If the ratio of entries to buffer size ever changes, this setting
+ // will need to be updated.
+ capacity: Math.pow(2, 15),
+ };
+
+ deepEqual(
+ activeConfiguration,
+ expectedConfiguration,
+ "The active configuration matches the new configuration."
+ );
+
+ info("Get the profile.");
+ const profile = Services.profiler.getProfileData();
+ deepEqual(
+ profile.meta.configuration,
+ expectedConfiguration,
+ "The configuration also matches on the profile meta object."
+ );
+ }
+
+ await Services.profiler.StopProfiler();
+
+ equal(
+ Services.profiler.activeConfiguration,
+ null,
+ "When the profile is off, there is no active configuration."
+ );
+});
diff --git a/tools/profiler/tests/xpcshell/test_addProfilerMarker.js b/tools/profiler/tests/xpcshell/test_addProfilerMarker.js
new file mode 100644
index 0000000000..b11545a41c
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_addProfilerMarker.js
@@ -0,0 +1,221 @@
+/* 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/. */
+
+/**
+ * Test that ChromeUtils.addProfilerMarker is working correctly.
+ */
+
+const markerNamePrefix = "test_addProfilerMarker";
+const markerText = "Text payload";
+// The same startTime will be used for all markers with a duration,
+// and we store this value globally so that expectDuration and
+// expectNoDuration can access it. The value isn't set here as we
+// want a start time after the profiler has started
+var startTime;
+
+function expectNoDuration(marker) {
+ Assert.equal(
+ typeof marker.startTime,
+ "number",
+ "startTime should be a number"
+ );
+ Assert.greater(
+ marker.startTime,
+ startTime,
+ "startTime should be after the begining of the test"
+ );
+ Assert.equal(typeof marker.endTime, "number", "endTime should be a number");
+ Assert.equal(marker.endTime, 0, "endTime should be 0");
+}
+
+function expectDuration(marker) {
+ Assert.equal(
+ typeof marker.startTime,
+ "number",
+ "startTime should be a number"
+ );
+ // Floats can cause rounding issues. We've seen up to a 4.17e-5 difference in
+ // intermittent failures, so we are permissive and accept up to 5e-5.
+ Assert.less(
+ Math.abs(marker.startTime - startTime),
+ 5e-5,
+ "startTime should be the expected time"
+ );
+ Assert.equal(typeof marker.endTime, "number", "endTime should be a number");
+ Assert.greater(
+ marker.endTime,
+ startTime,
+ "endTime should be after startTime"
+ );
+}
+
+function expectNoData(marker) {
+ Assert.equal(
+ typeof marker.data,
+ "undefined",
+ "The data property should be undefined"
+ );
+}
+
+function expectText(marker) {
+ Assert.equal(
+ typeof marker.data,
+ "object",
+ "The data property should be an object"
+ );
+ Assert.equal(marker.data.type, "Text", "Should be a Text marker");
+ Assert.equal(
+ marker.data.name,
+ markerText,
+ "The payload should contain the expected text"
+ );
+}
+
+function expectNoStack(marker) {
+ Assert.ok(!marker.data || !marker.data.stack, "There should be no stack");
+}
+
+function expectStack(marker, thread) {
+ let stack = marker.data.stack;
+ Assert.ok(!!stack, "There should be a stack");
+
+ // Marker stacks are recorded as a profile of a thread with a single sample,
+ // get the stack id.
+ stack = stack.samples.data[0][stack.samples.schema.stack];
+
+ const stackPrefixCol = thread.stackTable.schema.prefix;
+ const stackFrameCol = thread.stackTable.schema.frame;
+ const frameLocationCol = thread.frameTable.schema.location;
+
+ // Get the entire stack in an array for easier processing.
+ let result = [];
+ while (stack != null) {
+ let stackEntry = thread.stackTable.data[stack];
+ let frame = thread.frameTable.data[stackEntry[stackFrameCol]];
+ result.push(thread.stringTable[frame[frameLocationCol]]);
+ stack = stackEntry[stackPrefixCol];
+ }
+
+ Assert.greaterOrEqual(
+ result.length,
+ 1,
+ "There should be at least one frame in the stack"
+ );
+
+ Assert.ok(
+ result.some(frame => frame.includes("testMarker")),
+ "the 'testMarker' function should be visible in the stack"
+ );
+
+ Assert.ok(
+ !result.some(frame => frame.includes("ChromeUtils.addProfilerMarker")),
+ "the 'ChromeUtils.addProfilerMarker' label frame should not be visible in the stack"
+ );
+}
+
+add_task(async () => {
+ startProfilerForMarkerTests();
+ startTime = Cu.now();
+ while (Cu.now() < startTime + 1) {
+ // Busy wait for 1ms to ensure the intentionally set start time of markers
+ // will be significantly different from the time at which the marker is
+ // recorded.
+ }
+ info("startTime used for markers with durations: " + startTime);
+
+ /* Each call to testMarker will record a marker with a unique name.
+ * The testFunctions and testCases objects contain respectively test
+ * functions to verify that the marker found in the captured profile
+ * matches expectations, and a string that can be printed to describe
+ * in which way ChromeUtils.addProfilerMarker was called. */
+ let testFunctions = {};
+ let testCases = {};
+ let markerId = 0;
+ function testMarker(args, checks) {
+ let name = markerNamePrefix + markerId++;
+ ChromeUtils.addProfilerMarker(name, ...args);
+ testFunctions[name] = checks;
+ testCases[name] = `ChromeUtils.addProfilerMarker(${[name, ...args]
+ .toSource()
+ .slice(1, -1)})`;
+ }
+
+ info("Record markers without options object.");
+ testMarker([], m => {
+ expectNoDuration(m);
+ expectNoData(m);
+ });
+ testMarker([startTime], m => {
+ expectDuration(m);
+ expectNoData(m);
+ });
+ testMarker([undefined, markerText], m => {
+ expectNoDuration(m);
+ expectText(m);
+ });
+ testMarker([startTime, markerText], m => {
+ expectDuration(m);
+ expectText(m);
+ });
+
+ info("Record markers providing the duration as the startTime property.");
+ testMarker([{ startTime }], m => {
+ expectDuration(m);
+ expectNoData(m);
+ });
+ testMarker([{}, markerText], m => {
+ expectNoDuration(m);
+ expectText(m);
+ });
+ testMarker([{ startTime }, markerText], m => {
+ expectDuration(m);
+ expectText(m);
+ });
+
+ info("Record markers to test the captureStack property.");
+ const captureStack = true;
+ testMarker([], expectNoStack);
+ testMarker([startTime, markerText], expectNoStack);
+ testMarker([{ captureStack: false }], expectNoStack);
+ testMarker([{ captureStack }], expectStack);
+ testMarker([{ startTime, captureStack }], expectStack);
+ testMarker([{ captureStack }, markerText], expectStack);
+ testMarker([{ startTime, captureStack }, markerText], expectStack);
+
+ info("Record markers to test the category property");
+ function testCategory(args, expectedCategory) {
+ testMarker(args, marker => {
+ Assert.equal(marker.category, expectedCategory);
+ });
+ }
+ testCategory([], "JavaScript");
+ testCategory([{ category: "Test" }], "Test");
+ testCategory([{ category: "Test" }, markerText], "Test");
+ testCategory([{ category: "JavaScript" }], "JavaScript");
+ testCategory([{ category: "Other" }], "Other");
+ testCategory([{ category: "DOM" }], "DOM");
+ testCategory([{ category: "does not exist" }], "Other");
+
+ info("Capture the profile");
+ const profile = await stopNowAndGetProfile();
+ const mainThread = profile.threads.find(({ name }) => name === "GeckoMain");
+ const markers = getInflatedMarkerData(mainThread).filter(m =>
+ m.name.startsWith(markerNamePrefix)
+ );
+ Assert.equal(
+ markers.length,
+ Object.keys(testFunctions).length,
+ `Found ${markers.length} test markers in the captured profile`
+ );
+
+ for (let marker of markers) {
+ marker.category = profile.meta.categories[marker.category].name;
+ info(`${testCases[marker.name]} -> ${marker.toSource()}`);
+
+ testFunctions[marker.name](marker, mainThread);
+ delete testFunctions[marker.name];
+ }
+
+ Assert.equal(0, Object.keys(testFunctions).length, "all markers were found");
+});
diff --git a/tools/profiler/tests/xpcshell/test_asm.js b/tools/profiler/tests/xpcshell/test_asm.js
new file mode 100644
index 0000000000..ced36ce429
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_asm.js
@@ -0,0 +1,76 @@
+// Check that asm.js code shows up on the stack.
+add_task(async () => {
+ // This test assumes that it's starting on an empty profiler stack.
+ // (Note that the other profiler tests also assume the profiler
+ // isn't already started.)
+ Assert.ok(!Services.profiler.IsActive());
+
+ let jsFuns = Cu.getJSTestingFunctions();
+ if (!jsFuns.isAsmJSCompilationAvailable()) {
+ return;
+ }
+
+ const ms = 10;
+ await Services.profiler.StartProfiler(10000, ms, ["js"]);
+
+ let stack = null;
+ function ffi_function() {
+ var delayMS = 5;
+ while (1) {
+ let then = Date.now();
+ do {
+ // do nothing
+ } while (Date.now() - then < delayMS);
+
+ var thread0 = Services.profiler.getProfileData().threads[0];
+
+ if (delayMS > 30000) {
+ return;
+ }
+
+ delayMS *= 2;
+
+ if (!thread0.samples.data.length) {
+ continue;
+ }
+
+ var lastSample = thread0.samples.data[thread0.samples.data.length - 1];
+ stack = String(getInflatedStackLocations(thread0, lastSample));
+ if (stack.includes("trampoline")) {
+ return;
+ }
+ }
+ }
+
+ function asmjs_module(global, ffis) {
+ "use asm";
+ var ffi = ffis.ffi;
+ function asmjs_function() {
+ ffi();
+ }
+ return asmjs_function;
+ }
+
+ Assert.ok(jsFuns.isAsmJSModule(asmjs_module));
+
+ var asmjs_function = asmjs_module(null, { ffi: ffi_function });
+ Assert.ok(jsFuns.isAsmJSFunction(asmjs_function));
+
+ asmjs_function();
+
+ Assert.notEqual(stack, null);
+
+ var i1 = stack.indexOf("entry trampoline");
+ Assert.ok(i1 !== -1);
+ var i2 = stack.indexOf("asmjs_function");
+ Assert.ok(i2 !== -1);
+ var i3 = stack.indexOf("exit trampoline");
+ Assert.ok(i3 !== -1);
+ var i4 = stack.indexOf("ffi_function");
+ Assert.ok(i4 !== -1);
+ Assert.ok(i1 < i2);
+ Assert.ok(i2 < i3);
+ Assert.ok(i3 < i4);
+
+ await Services.profiler.StopProfiler();
+});
diff --git a/tools/profiler/tests/xpcshell/test_assertion_helper.js b/tools/profiler/tests/xpcshell/test_assertion_helper.js
new file mode 100644
index 0000000000..baa4c34818
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_assertion_helper.js
@@ -0,0 +1,162 @@
+add_task(function setup() {
+ // With the default reporter, an assertion doesn't throw if it fails, it
+ // merely report the result to the reporter and then go on. But in this test
+ // we want that a failure really throws, so that we can actually assert that
+ // it throws in case of failures!
+ // That's why we disable the default repoter here.
+ // I noticed that this line needs to be in an add_task (or possibly run_test)
+ // function. If put outside this will crash the test.
+ Assert.setReporter(null);
+});
+
+add_task(function test_objectContains() {
+ const fixture = {
+ foo: "foo",
+ bar: "bar",
+ };
+
+ Assert.objectContains(fixture, { foo: "foo" }, "Matches one property value");
+ Assert.objectContains(
+ fixture,
+ { foo: "foo", bar: "bar" },
+ "Matches both properties"
+ );
+ Assert.objectContainsOnly(
+ fixture,
+ { foo: "foo", bar: "bar" },
+ "Matches both properties"
+ );
+ Assert.throws(
+ () => Assert.objectContainsOnly(fixture, { foo: "foo" }),
+ /AssertionError/,
+ "Fails if some properties are missing"
+ );
+ Assert.throws(
+ () => Assert.objectContains(fixture, { foo: "bar" }),
+ /AssertionError/,
+ "Fails if the value for a present property is wrong"
+ );
+ Assert.throws(
+ () => Assert.objectContains(fixture, { hello: "world" }),
+ /AssertionError/,
+ "Fails if an expected property is missing"
+ );
+ Assert.throws(
+ () => Assert.objectContains(fixture, { foo: "foo", hello: "world" }),
+ /AssertionError/,
+ "Fails if some properties are present but others are missing"
+ );
+});
+
+add_task(function test_objectContains_expectations() {
+ const fixture = {
+ foo: "foo",
+ bar: "bar",
+ num: 42,
+ nested: {
+ nestedFoo: "nestedFoo",
+ nestedBar: "nestedBar",
+ },
+ };
+
+ Assert.objectContains(
+ fixture,
+ {
+ foo: Expect.stringMatches(/^fo/),
+ bar: Expect.stringContains("ar"),
+ num: Expect.number(),
+ nested: Expect.objectContainsOnly({
+ nestedFoo: Expect.stringMatches(/[Ff]oo/),
+ nestedBar: Expect.stringMatches(/[Bb]ar/),
+ }),
+ },
+ "Supports expectations"
+ );
+ Assert.objectContainsOnly(
+ fixture,
+ {
+ foo: Expect.stringMatches(/^fo/),
+ bar: Expect.stringContains("ar"),
+ num: Expect.number(),
+ nested: Expect.objectContains({
+ nestedFoo: Expect.stringMatches(/[Ff]oo/),
+ }),
+ },
+ "Supports expectations"
+ );
+
+ Assert.objectContains(fixture, {
+ num: val => Assert.greater(val, 40),
+ });
+
+ // Failed expectations
+ Assert.throws(
+ () =>
+ Assert.objectContains(fixture, {
+ foo: Expect.stringMatches(/bar/),
+ }),
+ /AssertionError/,
+ "Expect.stringMatches shouldn't match when the value is unexpected"
+ );
+ Assert.throws(
+ () =>
+ Assert.objectContains(fixture, {
+ foo: Expect.stringContains("bar"),
+ }),
+ /AssertionError/,
+ "Expect.stringContains shouldn't match when the value is unexpected"
+ );
+ Assert.throws(
+ () =>
+ Assert.objectContains(fixture, {
+ foo: Expect.number(),
+ }),
+ /AssertionError/,
+ "Expect.number shouldn't match when the value isn't a number"
+ );
+ Assert.throws(
+ () =>
+ Assert.objectContains(fixture, {
+ nested: Expect.objectContains({
+ nestedFoo: "bar",
+ }),
+ }),
+ /AssertionError/,
+ "Expect.objectContains should throw when the value is unexpected"
+ );
+
+ Assert.throws(
+ () =>
+ Assert.objectContains(fixture, {
+ num: val => Assert.less(val, 40),
+ }),
+ /AssertionError/,
+ "Expect.objectContains should throw when a function assertion fails"
+ );
+});
+
+add_task(function test_type_expectations() {
+ const fixture = {
+ any: "foo",
+ string: "foo",
+ number: 42,
+ boolean: true,
+ bigint: 42n,
+ symbol: Symbol("foo"),
+ object: { foo: "foo" },
+ function1() {},
+ function2: () => {},
+ };
+
+ Assert.objectContains(fixture, {
+ any: Expect.any(),
+ string: Expect.string(),
+ number: Expect.number(),
+ boolean: Expect.boolean(),
+ bigint: Expect.bigint(),
+ symbol: Expect.symbol(),
+ object: Expect.object(),
+ function1: Expect.function(),
+ function2: Expect.function(),
+ });
+});
diff --git a/tools/profiler/tests/xpcshell/test_enterjit_osr.js b/tools/profiler/tests/xpcshell/test_enterjit_osr.js
new file mode 100644
index 0000000000..86845ddc76
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_enterjit_osr.js
@@ -0,0 +1,52 @@
+// Check that the EnterJIT frame, added by the JIT trampoline and
+// usable by a native unwinder to resume unwinding after encountering
+// JIT code, is pushed as expected.
+function run_test() {
+ // This test assumes that it's starting on an empty profiler stack.
+ // (Note that the other profiler tests also assume the profiler
+ // isn't already started.)
+ Assert.ok(!Services.profiler.IsActive());
+
+ const ms = 5;
+ Services.profiler.StartProfiler(10000, ms, ["js"]);
+
+ function has_arbitrary_name_in_stack() {
+ // A frame for |arbitrary_name| has been pushed. Do a sequence of
+ // increasingly long spins until we get a sample.
+ var delayMS = 5;
+ while (1) {
+ info("loop: ms = " + delayMS);
+ const then = Date.now();
+ do {
+ let n = 10000;
+ // eslint-disable-next-line no-empty
+ while (--n) {} // OSR happens here
+ // Spin in the hope of getting a sample.
+ } while (Date.now() - then < delayMS);
+ let profile = Services.profiler.getProfileData().threads[0];
+
+ // Go through all of the stacks, and search for this function name.
+ for (const sample of profile.samples.data) {
+ const stack = getInflatedStackLocations(profile, sample);
+ info(`The following stack was found: ${stack}`);
+ for (var i = 0; i < stack.length; i++) {
+ if (stack[i].match(/arbitrary_name/)) {
+ // This JS sample was correctly found.
+ return true;
+ }
+ }
+ }
+
+ // Continue running this function with an increasingly long delay.
+ delayMS *= 2;
+ if (delayMS > 30000) {
+ return false;
+ }
+ }
+ }
+ Assert.ok(
+ has_arbitrary_name_in_stack(),
+ "A JS frame was found before the test timeout."
+ );
+ Services.profiler.StopProfiler();
+}
diff --git a/tools/profiler/tests/xpcshell/test_enterjit_osr_disabling.js b/tools/profiler/tests/xpcshell/test_enterjit_osr_disabling.js
new file mode 100644
index 0000000000..558c9b0c3b
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_enterjit_osr_disabling.js
@@ -0,0 +1,14 @@
+function run_test() {
+ Assert.ok(!Services.profiler.IsActive());
+
+ Services.profiler.StartProfiler(100, 10, ["js"]);
+ // The function is entered with the profiler enabled
+ (function () {
+ Services.profiler.StopProfiler();
+ let n = 10000;
+ // eslint-disable-next-line no-empty
+ while (--n) {} // OSR happens here with the profiler disabled.
+ // An assertion will fail when this function returns, if the
+ // profiler stack was misbalanced.
+ })();
+}
diff --git a/tools/profiler/tests/xpcshell/test_enterjit_osr_enabling.js b/tools/profiler/tests/xpcshell/test_enterjit_osr_enabling.js
new file mode 100644
index 0000000000..313d939caf
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_enterjit_osr_enabling.js
@@ -0,0 +1,14 @@
+function run_test() {
+ Assert.ok(!Services.profiler.IsActive());
+
+ // The function is entered with the profiler disabled.
+ (function () {
+ Services.profiler.StartProfiler(100, 10, ["js"]);
+ let n = 10000;
+ // eslint-disable-next-line no-empty
+ while (--n) {} // OSR happens here with the profiler enabled.
+ // An assertion will fail when this function returns, if the
+ // profiler stack was misbalanced.
+ })();
+ Services.profiler.StopProfiler();
+}
diff --git a/tools/profiler/tests/xpcshell/test_feature_fileioall.js b/tools/profiler/tests/xpcshell/test_feature_fileioall.js
new file mode 100644
index 0000000000..e5ac040b98
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_fileioall.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+add_task(async () => {
+ info(
+ "Test that off-main thread fileio is captured for a profiled thread, " +
+ "and that it will be sent to the main thread."
+ );
+ const filename = "test_marker_fileio";
+ const profile = await startProfilerAndTriggerFileIO({
+ features: ["fileioall"],
+ threadsFilter: ["GeckoMain", "BgIOThreadPool"],
+ filename,
+ });
+
+ const threads = getThreads(profile);
+ const mainThread = threads.find(thread => thread.name === "GeckoMain");
+ const mainThreadFileIO = getInflatedFileIOMarkers(mainThread, filename);
+ let backgroundThread;
+ let backgroundThreadFileIO;
+ for (const thread of threads) {
+ // Check for FileIO in any of the background threads.
+ if (thread.name.startsWith("BgIOThreadPool")) {
+ const markers = getInflatedFileIOMarkers(thread, filename);
+ if (markers.length) {
+ backgroundThread = thread;
+ backgroundThreadFileIO = markers;
+ break;
+ }
+ }
+ }
+
+ info("Check all of the main thread FileIO markers.");
+ checkInflatedFileIOMarkers(mainThreadFileIO, filename);
+ for (const { data, name } of mainThreadFileIO) {
+ equal(
+ name,
+ "FileIO (non-main thread)",
+ "The markers from off main thread are labeled as such."
+ );
+ equal(
+ data.threadId,
+ backgroundThread.tid,
+ "The main thread FileIO markers were all sent from the background thread."
+ );
+ }
+
+ info("Check all of the background thread FileIO markers.");
+ checkInflatedFileIOMarkers(backgroundThreadFileIO, filename);
+ for (const { data, name } of backgroundThreadFileIO) {
+ equal(
+ name,
+ "FileIO",
+ "The markers on the thread where they were generated just say FileIO"
+ );
+ equal(
+ data.threadId,
+ undefined,
+ "The background thread FileIO correctly excludes the threadId."
+ );
+ }
+});
+
+add_task(async () => {
+ info(
+ "Test that off-main thread fileio is captured for a thread that is not profiled, " +
+ "and that it will be sent to the main thread."
+ );
+ const filename = "test_marker_fileio";
+ const profile = await startProfilerAndTriggerFileIO({
+ features: ["fileioall"],
+ threadsFilter: ["GeckoMain"],
+ filename,
+ });
+
+ const threads = getThreads(profile);
+ const mainThread = threads.find(thread => thread.name === "GeckoMain");
+ const mainThreadFileIO = getInflatedFileIOMarkers(mainThread, filename);
+
+ info("Check all of the main thread FileIO markers.");
+ checkInflatedFileIOMarkers(mainThreadFileIO, filename);
+ for (const { data, name } of mainThreadFileIO) {
+ equal(
+ name,
+ "FileIO (non-profiled thread)",
+ "The markers from off main thread are labeled as such."
+ );
+ equal(typeof data.threadId, "number", "A thread ID is captured.");
+ }
+});
+
+/**
+ * @typedef {Object} TestConfig
+ * @prop {Array} features The list of profiler features
+ * @prop {string[]} threadsFilter The list of threads to profile
+ * @prop {string} filename A filename to trigger a write operation
+ */
+
+/**
+ * Start the profiler and get FileIO markers.
+ * @param {TestConfig}
+ * @returns {Profile}
+ */
+async function startProfilerAndTriggerFileIO({
+ features,
+ threadsFilter,
+ filename,
+}) {
+ const entries = 10000;
+ const interval = 10;
+ await Services.profiler.StartProfiler(
+ entries,
+ interval,
+ features,
+ threadsFilter
+ );
+
+ const path = PathUtils.join(PathUtils.tempDir, filename);
+
+ info(`Using a temporary file to test FileIO: ${path}`);
+
+ if (fileExists(path)) {
+ console.warn(
+ "This test is triggering FileIO by writing to a file. However, the test found an " +
+ "existing file at the location it was trying to write to. This could happen " +
+ "because a previous run of the test failed to clean up after itself. This test " +
+ " will now clean up that file before running the test again."
+ );
+ await removeFile(path);
+ }
+
+ info("Write to the file, but do so using a background thread.");
+
+ // IOUtils handles file operations using a background thread.
+ await IOUtils.write(path, new TextEncoder().encode("Test data."));
+ const exists = await fileExists(path);
+ ok(exists, `Created temporary file at: ${path}`);
+
+ info("Remove the file");
+ await removeFile(path);
+
+ return stopNowAndGetProfile();
+}
+
+async function fileExists(file) {
+ try {
+ let { type } = await IOUtils.stat(file);
+ return type === "regular";
+ } catch (_error) {
+ return false;
+ }
+}
+
+async function removeFile(file) {
+ await IOUtils.remove(file);
+ const exists = await fileExists(file);
+ ok(!exists, `Removed temporary file: ${file}`);
+}
diff --git a/tools/profiler/tests/xpcshell/test_feature_java.js b/tools/profiler/tests/xpcshell/test_feature_java.js
new file mode 100644
index 0000000000..e2f6879c2b
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_java.js
@@ -0,0 +1,31 @@
+/* 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/. */
+
+/**
+ * Test that Java capturing works as expected.
+ */
+add_task(async () => {
+ info("Test that Android Java sampler works as expected.");
+ const entries = 10000;
+ const interval = 1;
+ const threads = [];
+ const features = ["java"];
+
+ Services.profiler.StartProfiler(entries, interval, features, threads);
+ Assert.ok(Services.profiler.IsActive());
+
+ await captureAtLeastOneJsSample();
+
+ info(
+ "Stop the profiler and check that we have successfully captured a profile" +
+ " with the AndroidUI thread."
+ );
+ const profile = await stopNowAndGetProfile();
+ Assert.notEqual(profile, null);
+ const androidUiThread = profile.threads.find(
+ thread => thread.name == "AndroidUI (JVM)"
+ );
+ Assert.notEqual(androidUiThread, null);
+ Assert.ok(!Services.profiler.IsActive());
+});
diff --git a/tools/profiler/tests/xpcshell/test_feature_js.js b/tools/profiler/tests/xpcshell/test_feature_js.js
new file mode 100644
index 0000000000..a5949e4a0c
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_js.js
@@ -0,0 +1,63 @@
+/* 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/. */
+
+/**
+ * Test that JS capturing works as expected.
+ */
+add_task(async () => {
+ const entries = 10000;
+ const interval = 1;
+ const threads = [];
+ const features = ["js"];
+
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+
+ // Call the following to get a nice stack in the profiler:
+ // functionA -> functionB -> functionC -> captureAtLeastOneJsSample
+ const sampleIndex = await functionA();
+
+ const profile = await stopNowAndGetProfile();
+
+ const [thread] = profile.threads;
+ const { samples } = thread;
+
+ const inflatedStackFrames = getInflatedStackLocations(
+ thread,
+ samples.data[sampleIndex]
+ );
+
+ expectStackToContain(
+ inflatedStackFrames,
+ [
+ "(root)",
+ "js::RunScript",
+ // The following regexes match a string similar to:
+ //
+ // "functionA (/gecko/obj/_tests/xpcshell/tools/profiler/tests/xpcshell/test_feature_js.js:47:0)"
+ // or
+ // "functionA (test_feature_js.js:47:0)"
+ //
+ // this matches the script location
+ // | match the line number
+ // | | match the column number
+ // v v v
+ /^functionA \(.*test_feature_js\.js:\d+:\d+\)$/,
+ /^functionB \(.*test_feature_js\.js:\d+:\d+\)$/,
+ /^functionC \(.*test_feature_js\.js:\d+:\d+\)$/,
+ ],
+ "The stack contains a few frame labels, as well as the JS functions that we called."
+ );
+});
+
+function functionA() {
+ return functionB();
+}
+
+function functionB() {
+ return functionC();
+}
+
+async function functionC() {
+ return captureAtLeastOneJsSample();
+}
diff --git a/tools/profiler/tests/xpcshell/test_feature_mainthreadio.js b/tools/profiler/tests/xpcshell/test_feature_mainthreadio.js
new file mode 100644
index 0000000000..8ff5c9206d
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_mainthreadio.js
@@ -0,0 +1,122 @@
+/* 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/. */
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+/**
+ * Test that the IOInterposer is working correctly to capture main thread IO.
+ *
+ * This test should not run on release or beta, as the IOInterposer is wrapped in
+ * an ifdef.
+ */
+add_task(async () => {
+ {
+ const filename = "profiler-mainthreadio-test-firstrun";
+ const { markers, schema } = await runProfilerWithFileIO(
+ ["mainthreadio"],
+ filename
+ );
+ info("Check the FileIO markers when using the mainthreadio feature");
+ checkInflatedFileIOMarkers(markers, filename);
+
+ checkSchema(schema, {
+ name: "FileIO",
+ display: ["marker-chart", "marker-table", "timeline-fileio"],
+ data: [
+ {
+ key: "operation",
+ label: "Operation",
+ format: "string",
+ searchable: true,
+ },
+ { key: "source", label: "Source", format: "string", searchable: true },
+ {
+ key: "filename",
+ label: "Filename",
+ format: "file-path",
+ searchable: true,
+ },
+ {
+ key: "threadId",
+ label: "Thread ID",
+ format: "string",
+ searchable: true,
+ },
+ ],
+ });
+ }
+
+ {
+ const filename = "profiler-mainthreadio-test-no-instrumentation";
+ const { markers } = await runProfilerWithFileIO([], filename);
+ equal(
+ markers.length,
+ 0,
+ "No FileIO markers are found when the mainthreadio feature is not turned on " +
+ "in the profiler."
+ );
+ }
+
+ {
+ const filename = "profiler-mainthreadio-test-secondrun";
+ const { markers } = await runProfilerWithFileIO(["mainthreadio"], filename);
+ info("Check the FileIO markers when re-starting the mainthreadio feature");
+ checkInflatedFileIOMarkers(markers, filename);
+ }
+});
+
+/**
+ * Start the profiler and get FileIO markers and schema.
+ *
+ * @param {Array} features The list of profiler features
+ * @param {string} filename A filename to trigger a write operation
+ * @returns {{
+ * markers: InflatedMarkers[];
+ * schema: MarkerSchema;
+ * }}
+ */
+async function runProfilerWithFileIO(features, filename) {
+ const entries = 10000;
+ const interval = 10;
+ const threads = [];
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+
+ info("Get the file");
+ const file = FileUtils.getFile("TmpD", [filename]);
+ if (file.exists()) {
+ console.warn(
+ "This test is triggering FileIO by writing to a file. However, the test found an " +
+ "existing file at the location it was trying to write to. This could happen " +
+ "because a previous run of the test failed to clean up after itself. This test " +
+ " will now clean up that file before running the test again."
+ );
+ file.remove(false);
+ }
+
+ info(
+ "Generate file IO on the main thread using FileUtils.openSafeFileOutputStream."
+ );
+ const outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ const data = "Test data.";
+ info("Write to the file");
+ outputStream.write(data, data.length);
+
+ info("Close the file");
+ FileUtils.closeSafeFileOutputStream(outputStream);
+
+ info("Remove the file");
+ file.remove(false);
+
+ const profile = await stopNowAndGetProfile();
+ const mainThread = profile.threads.find(({ name }) => name === "GeckoMain");
+
+ const schema = getSchema(profile, "FileIO");
+
+ const markers = getInflatedFileIOMarkers(mainThread, filename);
+
+ return { schema, markers };
+}
diff --git a/tools/profiler/tests/xpcshell/test_feature_nativeallocations.js b/tools/profiler/tests/xpcshell/test_feature_nativeallocations.js
new file mode 100644
index 0000000000..64398d7ef9
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_nativeallocations.js
@@ -0,0 +1,158 @@
+/* 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/. */
+
+add_task(async () => {
+ if (!Services.profiler.GetFeatures().includes("nativeallocations")) {
+ Assert.ok(
+ true,
+ "Native allocations are not supported by this build, " +
+ "skip run the rest of the test."
+ );
+ return;
+ }
+
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ info(
+ "Test that the profiler can install memory hooks and collect native allocation " +
+ "information in the marker payloads."
+ );
+ {
+ info("Start the profiler.");
+ await startProfiler({
+ // Only instrument the main thread.
+ threads: ["GeckoMain"],
+ features: ["js", "nativeallocations"],
+ });
+
+ info(
+ "Do some JS work for a little bit. This will increase the amount of allocations " +
+ "that take place."
+ );
+ doWork();
+
+ info("Get the profile data and analyze it.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+
+ const {
+ allocationPayloads,
+ unmatchedAllocations,
+ logAllocationsAndDeallocations,
+ } = getAllocationInformation(profile);
+
+ Assert.greater(
+ allocationPayloads.length,
+ 0,
+ "Native allocation payloads were recorded for the parent process' main thread when " +
+ "the Native Allocation feature was turned on."
+ );
+
+ if (unmatchedAllocations.length !== 0) {
+ info(
+ "There were unmatched allocations. Log all of the allocations and " +
+ "deallocations in order to aid debugging."
+ );
+ logAllocationsAndDeallocations();
+ ok(
+ false,
+ "Found a deallocation that did not have a matching allocation site. " +
+ "This could happen if balanced allocations is broken, or if the the " +
+ "buffer size of this test was too small, and some markers ended up " +
+ "rolling off."
+ );
+ }
+
+ ok(true, "All deallocation sites had matching allocations.");
+ }
+
+ info("Restart the profiler, to ensure that we get no more allocations.");
+ {
+ await startProfiler({ features: ["js"] });
+ info("Do some work again.");
+ doWork();
+ info("Wait for the periodic sampling.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+ const allocationPayloads = getPayloadsOfType(
+ profile.threads[0],
+ "Native allocation"
+ );
+
+ Assert.equal(
+ allocationPayloads.length,
+ 0,
+ "No native allocations were collected when the feature was disabled."
+ );
+ }
+});
+
+function doWork() {
+ this.n = 0;
+ for (let i = 0; i < 1e5; i++) {
+ this.n += Math.random();
+ }
+}
+
+/**
+ * Extract the allocation payloads, and find the unmatched allocations.
+ */
+function getAllocationInformation(profile) {
+ // Get all of the allocation payloads.
+ const allocationPayloads = getPayloadsOfType(
+ profile.threads[0],
+ "Native allocation"
+ );
+
+ // Decide what is an allocation and deallocation.
+ const allocations = allocationPayloads.filter(
+ payload => ensureIsNumber(payload.size) >= 0
+ );
+ const deallocations = allocationPayloads.filter(
+ payload => ensureIsNumber(payload.size) < 0
+ );
+
+ // Now determine the unmatched allocations by building a set
+ const allocationSites = new Set(
+ allocations.map(({ memoryAddress }) => memoryAddress)
+ );
+
+ const unmatchedAllocations = deallocations.filter(
+ ({ memoryAddress }) => !allocationSites.has(memoryAddress)
+ );
+
+ // Provide a helper to log out the allocations and deallocations on failure.
+ function logAllocationsAndDeallocations() {
+ for (const { memoryAddress } of allocations) {
+ console.log("Allocations", formatHex(memoryAddress));
+ allocationSites.add(memoryAddress);
+ }
+
+ for (const { memoryAddress } of deallocations) {
+ console.log("Deallocations", formatHex(memoryAddress));
+ }
+
+ for (const { memoryAddress } of unmatchedAllocations) {
+ console.log("Deallocation with no allocation", formatHex(memoryAddress));
+ }
+ }
+
+ return {
+ allocationPayloads,
+ unmatchedAllocations,
+ logAllocationsAndDeallocations,
+ };
+}
+
+function ensureIsNumber(value) {
+ if (typeof value !== "number") {
+ throw new Error(`Expected a number: ${value}`);
+ }
+ return value;
+}
+
+function formatHex(number) {
+ return `0x${number.toString(16)}`;
+}
diff --git a/tools/profiler/tests/xpcshell/test_feature_stackwalking.js b/tools/profiler/tests/xpcshell/test_feature_stackwalking.js
new file mode 100644
index 0000000000..aa0bc86547
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_feature_stackwalking.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+/**
+ * Do a basic test to see if native frames are being collected for stackwalking. This
+ * test is fairly naive, as it does not attempt to check that these are valid symbols,
+ * only that some kind of stack walking is happening. It does this by making sure at
+ * least two native frames are collected.
+ */
+add_task(async () => {
+ const entries = 10000;
+ const interval = 1;
+ const threads = [];
+ const features = ["stackwalk"];
+
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+ const sampleIndex = await captureAtLeastOneJsSample();
+
+ const profile = await stopNowAndGetProfile();
+ const [thread] = profile.threads;
+ const { samples } = thread;
+
+ const inflatedStackFrames = getInflatedStackLocations(
+ thread,
+ samples.data[sampleIndex]
+ );
+ const nativeStack = /^0x[0-9a-f]+$/;
+
+ expectStackToContain(
+ inflatedStackFrames,
+ [
+ "(root)",
+ // There are probably more native stacks here.
+ nativeStack,
+ nativeStack,
+ // Since this is an xpcshell test we know that JavaScript will run:
+ "js::RunScript",
+ // There are probably more native stacks here.
+ nativeStack,
+ nativeStack,
+ ],
+ "Expected native stacks to be interleaved between some frame labels. There should" +
+ "be more than one native stack if stack walking is working correctly. There " +
+ "is no attempt here to determine if the memory addresses point to the correct " +
+ "symbols"
+ );
+});
diff --git a/tools/profiler/tests/xpcshell/test_get_features.js b/tools/profiler/tests/xpcshell/test_get_features.js
new file mode 100644
index 0000000000..e9bf0047c8
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_get_features.js
@@ -0,0 +1,8 @@
+/* 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/. */
+
+function run_test() {
+ var profilerFeatures = Services.profiler.GetFeatures();
+ Assert.ok(profilerFeatures != null);
+}
diff --git a/tools/profiler/tests/xpcshell/test_merged_stacks.js b/tools/profiler/tests/xpcshell/test_merged_stacks.js
new file mode 100644
index 0000000000..7f851e8de9
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_merged_stacks.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+/**
+ * Test that we correctly merge the three stack types, JS, native, and frame labels.
+ */
+add_task(async () => {
+ const entries = 10000;
+ const interval = 1;
+ const threads = [];
+ const features = ["js", "stackwalk"];
+
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+
+ // Call the following to get a nice stack in the profiler:
+ // functionA -> functionB -> functionC
+ const sampleIndex = await functionA();
+
+ const profile = await stopNowAndGetProfile();
+ const [thread] = profile.threads;
+ const { samples } = thread;
+
+ const inflatedStackFrames = getInflatedStackLocations(
+ thread,
+ samples.data[sampleIndex]
+ );
+
+ const nativeStack = /^0x[0-9a-f]+$/;
+
+ expectStackToContain(
+ inflatedStackFrames,
+ [
+ "(root)",
+ nativeStack,
+ nativeStack,
+ // There are more native stacks and frame labels here, but we know some execute
+ // and then the "js::RunScript" frame label runs.
+ "js::RunScript",
+ nativeStack,
+ nativeStack,
+ // The following regexes match a string similar to:
+ //
+ // "functionA (/gecko/obj/_tests/xpcshell/tools/profiler/tests/xpcshell/test_merged_stacks.js:47:0)"
+ // or
+ // "functionA (test_merged_stacks.js:47:0)"
+ //
+ // this matches the script location
+ // | match the line number
+ // | | match the column number
+ // v v v
+ /^functionA \(.*test_merged_stacks\.js:\d+:\d+\)$/,
+ /^functionB \(.*test_merged_stacks\.js:\d+:\d+\)$/,
+ /^functionC \(.*test_merged_stacks\.js:\d+:\d+\)$/,
+ // After the JS frames, then there are a bunch of arbitrary native stack frames
+ // that run.
+ nativeStack,
+ nativeStack,
+ ],
+ "The stack contains a few frame labels, as well as the JS functions that we called."
+ );
+});
+
+async function functionA() {
+ return functionB();
+}
+
+async function functionB() {
+ return functionC();
+}
+
+async function functionC() {
+ return captureAtLeastOneJsSample();
+}
diff --git a/tools/profiler/tests/xpcshell/test_pause.js b/tools/profiler/tests/xpcshell/test_pause.js
new file mode 100644
index 0000000000..0e621fb19f
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_pause.js
@@ -0,0 +1,126 @@
+/* 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/. */
+
+add_task(async () => {
+ Assert.ok(!Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+
+ let startPromise = Services.profiler.StartProfiler(1000, 10, []);
+
+ // Default: Active and not paused.
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ await startPromise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ // Pause everything, implicitly pauses sampling.
+ let pausePromise = Services.profiler.Pause();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await pausePromise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ // While fully paused, pause and resume sampling only, no expected changes.
+ let pauseSamplingPromise = Services.profiler.PauseSampling();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await pauseSamplingPromise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ let resumeSamplingPromise = Services.profiler.ResumeSampling();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await resumeSamplingPromise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ // Resume everything.
+ let resumePromise = Services.profiler.Resume();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ await resumePromise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ // Pause sampling only.
+ let pauseSampling2Promise = Services.profiler.PauseSampling();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await pauseSampling2Promise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ // While sampling is paused, pause everything.
+ let pause2Promise = Services.profiler.Pause();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await pause2Promise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ // Resume, but sampling is still paused separately.
+ let resume2promise = Services.profiler.Resume();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ await resume2promise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(Services.profiler.IsSamplingPaused());
+
+ // Resume sampling only.
+ let resumeSampling2Promise = Services.profiler.ResumeSampling();
+
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ await resumeSampling2Promise;
+ Assert.ok(Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ let stopPromise = Services.profiler.StopProfiler();
+ Assert.ok(!Services.profiler.IsActive());
+ // Stopping is not pausing.
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+
+ await stopPromise;
+ Assert.ok(!Services.profiler.IsActive());
+ Assert.ok(!Services.profiler.IsPaused());
+ Assert.ok(!Services.profiler.IsSamplingPaused());
+});
diff --git a/tools/profiler/tests/xpcshell/test_responsiveness.js b/tools/profiler/tests/xpcshell/test_responsiveness.js
new file mode 100644
index 0000000000..5f57173090
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_responsiveness.js
@@ -0,0 +1,50 @@
+/* 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/. */
+
+/**
+ * Test that we can measure non-zero event delays
+ */
+
+add_task(async () => {
+ const entries = 10000;
+ const interval = 1;
+ const threads = [];
+ const features = [];
+
+ await Services.profiler.StartProfiler(entries, interval, features, threads);
+
+ await functionA();
+
+ const profile = await stopNowAndGetProfile();
+ const [thread] = profile.threads;
+ const { samples } = thread;
+ const message = "eventDelay > 0 not found.";
+ let SAMPLE_STACK_SLOT = thread.samples.schema.eventDelay;
+
+ for (let i = 0; i < samples.data.length; i++) {
+ if (samples.data[i][SAMPLE_STACK_SLOT] > 0) {
+ Assert.ok(true, message);
+ return;
+ }
+ }
+ Assert.ok(false, message);
+});
+
+function doSyncWork(milliseconds) {
+ const start = Date.now();
+ while (true) {
+ this.n = 0;
+ for (let i = 0; i < 1e5; i++) {
+ this.n += Math.random();
+ }
+ if (Date.now() - start > milliseconds) {
+ return;
+ }
+ }
+}
+
+async function functionA() {
+ doSyncWork(100);
+ return captureAtLeastOneJsSample();
+}
diff --git a/tools/profiler/tests/xpcshell/test_run.js b/tools/profiler/tests/xpcshell/test_run.js
new file mode 100644
index 0000000000..0e30edfd4e
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_run.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+function run_test() {
+ Assert.ok(!Services.profiler.IsActive());
+
+ Services.profiler.StartProfiler(1000, 10, []);
+
+ Assert.ok(Services.profiler.IsActive());
+
+ do_test_pending();
+
+ do_timeout(1000, function wait() {
+ // Check text profile format
+ var profileStr = Services.profiler.GetProfile();
+ Assert.ok(profileStr.length > 10);
+
+ // check json profile format
+ var profileObj = Services.profiler.getProfileData();
+ Assert.notEqual(profileObj, null);
+ Assert.notEqual(profileObj.threads, null);
+ // We capture memory counters by default only when jemalloc is turned
+ // on (and it isn't for ASAN), so unless we can conditionalize for ASAN
+ // here we can't check that we're capturing memory counter data.
+ Assert.notEqual(profileObj.counters, null);
+ Assert.notEqual(profileObj.memory, null);
+ Assert.ok(profileObj.threads.length >= 1);
+ Assert.notEqual(profileObj.threads[0].samples, null);
+ // NOTE: The number of samples will be empty since we
+ // don't have any labels in the xpcshell code
+
+ Services.profiler.StopProfiler();
+ Assert.ok(!Services.profiler.IsActive());
+ do_test_finished();
+ });
+}
diff --git a/tools/profiler/tests/xpcshell/test_shared_library.js b/tools/profiler/tests/xpcshell/test_shared_library.js
new file mode 100644
index 0000000000..e211ca642b
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_shared_library.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+function run_test() {
+ var libs = Services.profiler.sharedLibraries;
+
+ Assert.equal(typeof libs, "object");
+ Assert.ok(Array.isArray(libs));
+ Assert.equal(typeof libs, "object");
+ Assert.ok(libs.length >= 1);
+ Assert.equal(typeof libs[0], "object");
+ Assert.equal(typeof libs[0].name, "string");
+ Assert.equal(typeof libs[0].path, "string");
+ Assert.equal(typeof libs[0].debugName, "string");
+ Assert.equal(typeof libs[0].debugPath, "string");
+ Assert.equal(typeof libs[0].arch, "string");
+ Assert.equal(typeof libs[0].start, "number");
+ Assert.equal(typeof libs[0].end, "number");
+ Assert.ok(libs[0].start <= libs[0].end);
+}
diff --git a/tools/profiler/tests/xpcshell/test_start.js b/tools/profiler/tests/xpcshell/test_start.js
new file mode 100644
index 0000000000..c9ae135eb8
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/test_start.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+add_task(async () => {
+ Assert.ok(!Services.profiler.IsActive());
+
+ let startPromise = Services.profiler.StartProfiler(10, 100, []);
+
+ Assert.ok(Services.profiler.IsActive());
+
+ await startPromise;
+ Assert.ok(Services.profiler.IsActive());
+
+ let stopPromise = Services.profiler.StopProfiler();
+
+ Assert.ok(!Services.profiler.IsActive());
+
+ await stopPromise;
+ Assert.ok(!Services.profiler.IsActive());
+});
diff --git a/tools/profiler/tests/xpcshell/xpcshell.ini b/tools/profiler/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..a7c461b4ac
--- /dev/null
+++ b/tools/profiler/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+head = head.js
+support-files =
+ ../shared-head.js
+
+[test_active_configuration.js]
+skip-if = tsan # Intermittent timeouts, bug 1781449
+[test_addProfilerMarker.js]
+[test_start.js]
+skip-if = true
+[test_get_features.js]
+[test_responsiveness.js]
+skip-if = tsan # Times out on TSan, bug 1612707
+[test_shared_library.js]
+[test_run.js]
+skip-if = true
+[test_pause.js]
+[test_enterjit_osr.js]
+[test_enterjit_osr_disabling.js]
+skip-if = !debug
+[test_enterjit_osr_enabling.js]
+skip-if = !debug
+[test_asm.js]
+[test_feature_mainthreadio.js]
+skip-if =
+ release_or_beta
+ os == "win" && socketprocess_networking
+[test_feature_fileioall.js]
+skip-if =
+ release_or_beta
+
+# The sanitizer checks appears to overwrite our own memory hooks in xpcshell tests,
+# and no allocation markers are gathered. Skip this test in that configuration.
+[test_feature_nativeallocations.js]
+skip-if =
+ os == "android" && verify # bug 1757528
+ asan
+ tsan
+ socketprocess_networking
+
+# Native stackwalking is somewhat unreliable depending on the platform.
+#
+# We don't have frame pointers on macOS release and beta, so stack walking does not
+# work. See Bug 1571216 for more details.
+#
+# Linux can be very unreliable when native stackwalking through JavaScript code.
+# See Bug 1434402 for more details.
+#
+# For sanitizer builds, there were many intermittents, and we're not getting much
+# additional coverage there, so it's better to be a bit more reliable.
+[test_feature_stackwalking.js]
+skip-if =
+ os == "mac" && release_or_beta
+ os == "linux" && release_or_beta && !debug
+ asan
+ tsan
+
+[test_feature_js.js]
+skip-if = tsan # Times out on TSan, bug 1612707
+
+# See the comment on test_feature_stackwalking.js
+[test_merged_stacks.js]
+skip-if =
+ os == "mac" && release_or_beta
+ os == "linux" && release_or_beta && !debug
+ asan
+ tsan
+
+[test_assertion_helper.js]
+[test_feature_java.js]
+skip-if =
+ os != "android"