summaryrefslogtreecommitdiffstats
path: root/tools/profiler/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/profiler/tests/browser/browser.ini102
-rw-r--r--tools/profiler/tests/browser/browser_test_feature_ipcmessages.js109
-rw-r--r--tools/profiler/tests/browser/browser_test_feature_jsallocations.js78
-rw-r--r--tools/profiler/tests/browser/browser_test_feature_nostacksampling.js76
-rw-r--r--tools/profiler/tests/browser/browser_test_feature_preferencereads.js116
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_cancel.js71
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_private_browsing.js91
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_redirect.js341
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_serviceworker_cache_first.js388
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_fetch_handler.js218
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_respondWith_in_fetch_handler.js296
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_serviceworker_synthetized_response.js558
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_simple.js81
-rw-r--r--tools/profiler/tests/browser/browser_test_marker_network_sts.js130
-rw-r--r--tools/profiler/tests/browser/browser_test_markers_gc_cc.js49
-rw-r--r--tools/profiler/tests/browser/browser_test_markers_parent_process.js37
-rw-r--r--tools/profiler/tests/browser/browser_test_profile_capture_by_pid.js206
-rw-r--r--tools/profiler/tests/browser/browser_test_profile_fission.js196
-rw-r--r--tools/profiler/tests/browser/browser_test_profile_multi_frame_page_info.js83
-rw-r--r--tools/profiler/tests/browser/browser_test_profile_single_frame_page_info.js134
-rw-r--r--tools/profiler/tests/browser/browser_test_profile_slow_capture.js104
-rw-r--r--tools/profiler/tests/browser/do_work_500ms.html41
-rw-r--r--tools/profiler/tests/browser/firefox-logo-nightly.svg1
-rw-r--r--tools/profiler/tests/browser/head.js159
-rw-r--r--tools/profiler/tests/browser/multi_frame.html11
-rw-r--r--tools/profiler/tests/browser/page_with_resources.html11
-rw-r--r--tools/profiler/tests/browser/redirect.sjs8
-rw-r--r--tools/profiler/tests/browser/serviceworkers/firefox-logo-nightly.svg1
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker-utils.js39
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_cache_first.js34
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_no_fetch_handler.js4
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_no_respondWith_in_fetch_handler.js9
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_page.html10
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_register.html9
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_simple.html9
-rw-r--r--tools/profiler/tests/browser/serviceworkers/serviceworker_synthetized_response.js27
-rw-r--r--tools/profiler/tests/browser/simple.html9
-rw-r--r--tools/profiler/tests/browser/single_frame.html10
-rw-r--r--tools/profiler/tests/chrome/chrome.ini8
-rw-r--r--tools/profiler/tests/chrome/profiler_test_utils.js66
-rw-r--r--tools/profiler/tests/chrome/test_profile_worker.html66
-rw-r--r--tools/profiler/tests/chrome/test_profile_worker_bug_1428076.html58
-rw-r--r--tools/profiler/tests/gtest/GeckoProfiler.cpp4974
-rw-r--r--tools/profiler/tests/gtest/LulTest.cpp51
-rw-r--r--tools/profiler/tests/gtest/LulTestDwarf.cpp2733
-rw-r--r--tools/profiler/tests/gtest/LulTestInfrastructure.cpp498
-rw-r--r--tools/profiler/tests/gtest/LulTestInfrastructure.h735
-rw-r--r--tools/profiler/tests/gtest/ThreadProfileTest.cpp60
-rw-r--r--tools/profiler/tests/gtest/moz.build45
-rw-r--r--tools/profiler/tests/shared-head.js568
-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
72 files changed, 15606 insertions, 0 deletions
diff --git a/tools/profiler/tests/browser/browser.ini b/tools/profiler/tests/browser/browser.ini
new file mode 100644
index 0000000000..9a02b673d2
--- /dev/null
+++ b/tools/profiler/tests/browser/browser.ini
@@ -0,0 +1,102 @@
+[DEFAULT]
+skip-if = tsan # Bug 1804081 - TSan times out on pretty much all of these tests
+support-files =
+ ../shared-head.js
+ head.js
+
+[browser_test_feature_ipcmessages.js]
+support-files = simple.html
+
+[browser_test_feature_jsallocations.js]
+support-files = do_work_500ms.html
+
+[browser_test_feature_nostacksampling.js]
+support-files = do_work_500ms.html
+
+[browser_test_feature_preferencereads.js]
+support-files = single_frame.html
+
+[browser_test_markers_parent_process.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+[browser_test_markers_gc_cc.js]
+
+[browser_test_profile_capture_by_pid.js]
+skip-if = os == "win" && os_version == "6.1" # No thread names on win7, needed for these tests
+https_first_disabled = true
+support-files = single_frame.html
+
+[browser_test_profile_fission.js]
+support-files = single_frame.html
+
+[browser_test_profile_single_frame_page_info.js]
+https_first_disabled = true
+support-files = single_frame.html
+
+[browser_test_profile_slow_capture.js]
+https_first_disabled = true
+support-files = single_frame.html
+skip-if = !debug
+
+[browser_test_profile_multi_frame_page_info.js]
+https_first_disabled = true
+support-files =
+ multi_frame.html
+ single_frame.html
+
+[browser_test_marker_network_simple.js]
+https_first_disabled = true
+support-files = simple.html
+
+[browser_test_marker_network_private_browsing.js]
+support-files = simple.html
+
+[browser_test_marker_network_cancel.js]
+https_first_disabled = true
+support-files = simple.html
+
+[browser_test_marker_network_sts.js]
+support-files = simple.html
+
+[browser_test_marker_network_redirect.js]
+https_first_disabled = true
+support-files =
+ redirect.sjs
+ simple.html
+ page_with_resources.html
+ firefox-logo-nightly.svg
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+
+[browser_test_marker_network_serviceworker_cache_first.js]
+support-files =
+ serviceworkers/serviceworker-utils.js
+ serviceworkers/serviceworker_register.html
+ serviceworkers/serviceworker_page.html
+ serviceworkers/firefox-logo-nightly.svg
+ serviceworkers/serviceworker_cache_first.js
+
+[browser_test_marker_network_serviceworker_no_fetch_handler.js]
+support-files =
+ serviceworkers/serviceworker-utils.js
+ serviceworkers/serviceworker_register.html
+ serviceworkers/serviceworker_page.html
+ serviceworkers/firefox-logo-nightly.svg
+ serviceworkers/serviceworker_no_fetch_handler.js
+
+[browser_test_marker_network_serviceworker_no_respondWith_in_fetch_handler.js]
+support-files =
+ serviceworkers/serviceworker-utils.js
+ serviceworkers/serviceworker_register.html
+ serviceworkers/serviceworker_page.html
+ serviceworkers/firefox-logo-nightly.svg
+ serviceworkers/serviceworker_no_respondWith_in_fetch_handler.js
+
+[browser_test_marker_network_serviceworker_synthetized_response.js]
+support-files =
+ serviceworkers/serviceworker-utils.js
+ serviceworkers/serviceworker_register.html
+ serviceworkers/serviceworker_simple.html
+ serviceworkers/firefox-logo-nightly.svg
+ serviceworkers/serviceworker_synthetized_response.js
diff --git a/tools/profiler/tests/browser/browser_test_feature_ipcmessages.js b/tools/profiler/tests/browser/browser_test_feature_ipcmessages.js
new file mode 100644
index 0000000000..4691be6434
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_feature_ipcmessages.js
@@ -0,0 +1,109 @@
+/* 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/. */
+
+requestLongerTimeout(10);
+
+async function waitForLoad() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(function(resolve) {
+ if (content.document.readyState !== "complete") {
+ content.document.addEventListener("readystatechange", () => {
+ if (content.document.readyState === "complete") {
+ resolve();
+ }
+ });
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Test the IPCMessages feature.
+ */
+add_task(async function test_profile_feature_ipcmessges() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ const url = BASE_URL + "simple.html";
+
+ info("Open a tab while profiling IPC messages.");
+ await startProfiler({ features: ["js", "ipcmessages"] });
+ info("Started the profiler sucessfully! Now, let's open a tab.");
+
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ info("We opened a tab!");
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+ info("Now let's wait until it's fully loaded.");
+ await waitForLoad();
+
+ info(
+ "Check that some IPC profile markers were generated when " +
+ "the feature is enabled."
+ );
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await waitSamplingAndStopProfilerAndGetThreads(contentPid);
+
+ Assert.greater(
+ getPayloadsOfType(parentThread, "IPC").length,
+ 0,
+ "IPC profile markers were recorded for the parent process' main " +
+ "thread when the IPCMessages feature was turned on."
+ );
+
+ Assert.greater(
+ getPayloadsOfType(contentThread, "IPC").length,
+ 0,
+ "IPC profile markers were recorded for the content process' main " +
+ "thread when the IPCMessages feature was turned on."
+ );
+ }
+ });
+
+ info("Now open a tab without profiling IPC messages.");
+ await startProfiler({ features: ["js"] });
+
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+ await waitForLoad();
+
+ info(
+ "Check that no IPC profile markers were recorded when the " +
+ "feature is turned off."
+ );
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await waitSamplingAndStopProfilerAndGetThreads(contentPid);
+ Assert.equal(
+ getPayloadsOfType(parentThread, "IPC").length,
+ 0,
+ "No IPC profile markers were recorded for the parent process' main " +
+ "thread when the IPCMessages feature was turned off."
+ );
+
+ Assert.equal(
+ getPayloadsOfType(contentThread, "IPC").length,
+ 0,
+ "No IPC profile markers were recorded for the content process' main " +
+ "thread when the IPCMessages feature was turned off."
+ );
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_feature_jsallocations.js b/tools/profiler/tests/browser/browser_test_feature_jsallocations.js
new file mode 100644
index 0000000000..0ae3b53a14
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_feature_jsallocations.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+requestLongerTimeout(10);
+
+/**
+ * Test the JS Allocations feature. This is done as a browser test to ensure that
+ * we realistically try out how the JS allocations are running. This ensures that
+ * we are collecting allocations for the content process and the parent process.
+ */
+add_task(async function test_profile_feature_jsallocations() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ await startProfiler({ features: ["js", "jsallocations"] });
+
+ const url = BASE_URL + "do_work_500ms.html";
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ // Wait 500ms so that the tab finishes executing.
+ await wait(500);
+
+ // Check that we can get some allocations when the feature is turned on.
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await waitSamplingAndStopProfilerAndGetThreads(contentPid);
+ Assert.greater(
+ getPayloadsOfType(parentThread, "JS allocation").length,
+ 0,
+ "Allocations were recorded for the parent process' main thread when the " +
+ "JS Allocation feature was turned on."
+ );
+ Assert.greater(
+ getPayloadsOfType(contentThread, "JS allocation").length,
+ 0,
+ "Allocations were recorded for the content process' main thread when the " +
+ "JS Allocation feature was turned on."
+ );
+ }
+
+ await startProfiler({ features: ["js"] });
+ // Now reload the tab with a clean run.
+ gBrowser.reload();
+ await wait(500);
+
+ // Check that no allocations were recorded, and allocation tracking was correctly
+ // turned off.
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await waitSamplingAndStopProfilerAndGetThreads(contentPid);
+ Assert.equal(
+ getPayloadsOfType(parentThread, "JS allocation").length,
+ 0,
+ "No allocations were recorded for the parent processes' main thread when " +
+ "JS allocation was not turned on."
+ );
+
+ Assert.equal(
+ getPayloadsOfType(contentThread, "JS allocation").length,
+ 0,
+ "No allocations were recorded for the content processes' main thread when " +
+ "JS allocation was not turned on."
+ );
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_feature_nostacksampling.js b/tools/profiler/tests/browser/browser_test_feature_nostacksampling.js
new file mode 100644
index 0000000000..695e6031aa
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_feature_nostacksampling.js
@@ -0,0 +1,76 @@
+/* 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 the No Stack Sampling feature.
+ */
+add_task(async function test_profile_feature_nostacksampling() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ await startProfiler({ features: ["js", "nostacksampling"] });
+
+ const url = BASE_URL + "do_work_500ms.html";
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ // Wait 500ms so that the tab finishes executing.
+ await wait(500);
+
+ // Check that we can get no stacks when the feature is turned on.
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+ Assert.equal(
+ parentThread.samples.data.length,
+ 0,
+ "Stack samples were recorded from the parent process' main thread" +
+ "when the No Stack Sampling feature was turned on."
+ );
+ Assert.equal(
+ contentThread.samples.data.length,
+ 0,
+ "Stack samples were recorded from the content process' main thread" +
+ "when the No Stack Sampling feature was turned on."
+ );
+ }
+
+ // Flush out any straggling allocation markers that may have not been collected
+ // yet by starting and stopping the profiler once.
+ await startProfiler({ features: ["js"] });
+
+ // Now reload the tab with a clean run.
+ gBrowser.reload();
+ await wait(500);
+
+ // Check that stack samples were recorded.
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await waitSamplingAndStopProfilerAndGetThreads(contentPid);
+ Assert.greater(
+ parentThread.samples.data.length,
+ 0,
+ "No Stack samples were recorded from the parent process' main thread" +
+ "when the No Stack Sampling feature was not turned on."
+ );
+
+ Assert.greater(
+ contentThread.samples.data.length,
+ 0,
+ "No Stack samples were recorded from the content process' main thread" +
+ "when the No Stack Sampling feature was not turned on."
+ );
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_feature_preferencereads.js b/tools/profiler/tests/browser/browser_test_feature_preferencereads.js
new file mode 100644
index 0000000000..d0f16054d8
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_feature_preferencereads.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+requestLongerTimeout(10);
+
+const kContentPref = "font.size.variable.x-western";
+
+function countPrefReadsInThread(pref, thread) {
+ let count = 0;
+ for (let payload of getPayloadsOfType(thread, "PreferenceRead")) {
+ if (payload.prefName === pref) {
+ count++;
+ }
+ }
+ return count;
+}
+
+async function waitForPaintAfterLoad() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(function(resolve) {
+ function listener() {
+ if (content.document.readyState == "complete") {
+ content.requestAnimationFrame(() => content.setTimeout(resolve, 0));
+ }
+ }
+ if (content.document.readyState != "complete") {
+ content.document.addEventListener("readystatechange", listener);
+ } else {
+ listener();
+ }
+ });
+ });
+}
+
+/**
+ * Test the PreferenceRead feature.
+ */
+add_task(async function test_profile_feature_preferencereads() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ await startProfiler({ features: ["js", "preferencereads"] });
+
+ const url = BASE_URL + "single_frame.html";
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ await waitForPaintAfterLoad();
+
+ // Ensure we read a pref in the content process.
+ await SpecialPowers.spawn(contentBrowser, [kContentPref], pref => {
+ Services.prefs.getIntPref(pref);
+ });
+
+ // Check that some PreferenceRead profile markers were generated when the
+ // feature is enabled.
+ {
+ const { contentThread } = await stopProfilerNowAndGetThreads(contentPid);
+
+ Assert.greater(
+ countPrefReadsInThread(kContentPref, contentThread),
+ 0,
+ `PreferenceRead profile markers for ${kContentPref} were recorded ` +
+ "when the PreferenceRead feature was turned on."
+ );
+ }
+
+ await startProfiler({ features: ["js"] });
+ // Now reload the tab with a clean run.
+ await ContentTask.spawn(contentBrowser, null, () => {
+ return new Promise(resolve => {
+ addEventListener("pageshow", () => resolve(), {
+ capturing: true,
+ once: true,
+ });
+ content.location.reload();
+ });
+ });
+
+ await waitForPaintAfterLoad();
+
+ // Ensure we read a pref in the content process.
+ await SpecialPowers.spawn(contentBrowser, [kContentPref], pref => {
+ Services.prefs.getIntPref(pref);
+ });
+
+ // Check that no PreferenceRead markers were recorded when the feature
+ // is turned off.
+ {
+ const {
+ parentThread,
+ contentThread,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+ Assert.equal(
+ getPayloadsOfType(parentThread, "PreferenceRead").length,
+ 0,
+ "No PreferenceRead profile were recorded " +
+ "when the PreferenceRead feature was turned off."
+ );
+
+ Assert.equal(
+ getPayloadsOfType(contentThread, "PreferenceRead").length,
+ 0,
+ "No PreferenceRead profile were recorded " +
+ "when the PreferenceRead feature was turned off."
+ );
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_cancel.js b/tools/profiler/tests/browser/browser_test_marker_network_cancel.js
new file mode 100644
index 0000000000..ee7f1ef3ca
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_cancel.js
@@ -0,0 +1,71 @@
+/* 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 emit network markers with the cancel status.
+ */
+add_task(async function test_network_markers_early_cancel() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = BASE_URL + "simple.html?cacheBust=" + Math.random();
+ const options = {
+ gBrowser,
+ url: "about:blank",
+ waitForLoad: false,
+ };
+
+ const tab = await BrowserTestUtils.openNewForegroundTab(options);
+ const loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt(url, tab);
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+ const contentPid = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+ await loadPromise;
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+
+ info("parent process: " + JSON.stringify(parentNetworkMarkers, null, 2));
+ info("content process: " + JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 2,
+ `We should get a pair of network markers in the parent thread.`
+ );
+
+ // We don't test the markers in the content process, because depending on some
+ // timing we can have 0 or 1 (and maybe even 2 (?)).
+
+ const parentStopMarker = parentNetworkMarkers[1];
+
+ const expectedProperties = {
+ name: Expect.stringMatches(`Load \\d+:.*${escapeStringRegexp(url)}`),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_CANCEL",
+ URI: url,
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ cache: "Unresolved",
+ }),
+ };
+
+ Assert.objectContains(parentStopMarker, expectedProperties);
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_private_browsing.js b/tools/profiler/tests/browser/browser_test_marker_network_private_browsing.js
new file mode 100644
index 0000000000..0a10ea7157
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_private_browsing.js
@@ -0,0 +1,91 @@
+/* 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 emit network markers accordingly
+ */
+add_task(async function test_network_markers() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ fission: true,
+ });
+ try {
+ const url = BASE_URL_HTTPS + "simple.html?cacheBust=" + Math.random();
+ const contentBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(contentBrowser, url);
+ await BrowserTestUtils.browserLoaded(contentBrowser, false, url);
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ info(JSON.stringify(parentNetworkMarkers, null, 2));
+ info(JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 2,
+ `We should get a pair of network markers in the parent thread.`
+ );
+ Assert.equal(
+ contentNetworkMarkers.length,
+ 2,
+ `We should get a pair of network markers in the content thread.`
+ );
+
+ const parentStopMarker = parentNetworkMarkers[1];
+ const contentStopMarker = contentNetworkMarkers[1];
+
+ const expectedProperties = {
+ name: Expect.stringMatches(`Load \\d+:.*${escapeStringRegexp(url)}`),
+ data: Expect.objectContains({
+ status: "STATUS_STOP",
+ URI: url,
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ isPrivateBrowsing: true,
+ }),
+ };
+
+ Assert.objectContains(parentStopMarker, expectedProperties);
+ // The cache information is missing from the content marker, it's only part
+ // of the parent marker. See Bug 1544821.
+ Assert.objectContains(parentStopMarker.data, {
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+ Assert.objectContains(contentStopMarker, expectedProperties);
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_redirect.js b/tools/profiler/tests/browser/browser_test_marker_network_redirect.js
new file mode 100644
index 0000000000..28478c2b3b
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_redirect.js
@@ -0,0 +1,341 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test the redirect cases.
+ */
+add_task(async function test_network_markers_service_worker_setup() {
+ // Disabling cache makes the result more predictable especially in verify mode.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_redirect_simple() {
+ // In this test, we request an HTML page that gets redirected. This is a
+ // top-level navigation.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const targetFileNameWithCacheBust = "simple.html";
+ const url =
+ BASE_URL +
+ "redirect.sjs?" +
+ encodeURIComponent(targetFileNameWithCacheBust);
+ const targetUrl = BASE_URL + targetFileNameWithCacheBust;
+
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ info(JSON.stringify(parentNetworkMarkers, null, 2));
+ info(JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 4,
+ `We should get 2 pairs of network markers in the parent thread.`
+ );
+
+ /* It looks like that for a redirection for the top level navigation, the
+ * content thread sees the markers for the second request only.
+ * See Bug 1692879. */
+ Assert.equal(
+ contentNetworkMarkers.length,
+ 2,
+ `We should get one pair of network markers in the content thread.`
+ );
+
+ const parentRedirectMarker = parentNetworkMarkers[1];
+ const parentStopMarker = parentNetworkMarkers[3];
+ // There's no content redirect marker for the reason outlined above.
+ const contentStopMarker = contentNetworkMarkers[1];
+
+ Assert.objectContains(parentRedirectMarker, {
+ name: Expect.stringMatches(`Load \\d+:.*${escapeStringRegexp(url)}`),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: url,
+ RedirectURI: targetUrl,
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ redirectId: parentStopMarker.data.id,
+ pri: Expect.number(),
+ cache: Expect.stringMatches(/Missed|Unresolved/),
+ redirectType: "Permanent",
+ isHttpToHttpsRedirect: false,
+ }),
+ });
+
+ const expectedProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(targetUrl)}`
+ ),
+ };
+ const expectedDataProperties = {
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: targetUrl,
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ };
+
+ Assert.objectContains(parentStopMarker, expectedProperties);
+ Assert.objectContains(contentStopMarker, expectedProperties);
+
+ // The cache information is missing from the content marker, it's only part
+ // of the parent marker. See Bug 1544821.
+ Assert.objectContainsOnly(parentStopMarker.data, {
+ ...expectedDataProperties,
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+ Assert.objectContainsOnly(contentStopMarker.data, expectedDataProperties);
+ });
+});
+
+add_task(async function test_network_markers_redirect_resources() {
+ // In this test we request an HTML file that itself contains resources that
+ // are redirected.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = BASE_URL + "page_with_resources.html?cacheBust=" + Math.random();
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ info(JSON.stringify(parentNetworkMarkers, null, 2));
+ info(JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 8,
+ `We should get 4 pairs of network markers in the parent thread.`
+ // 1 - The main page
+ // 2 - The SVG
+ // 3 - The redirected request for the second SVG request.
+ // 4 - The SVG, again
+ );
+
+ /* In this second test, the top level navigation request isn't redirected.
+ * Contrary to Bug 1692879 we get all network markers for redirected
+ * resources. */
+ Assert.equal(
+ contentNetworkMarkers.length,
+ 8,
+ `We should get 4 pairs of network markers in the content thread.`
+ );
+
+ // The same resource firefox-logo-nightly.svg is requested twice, but the
+ // second time it is redirected.
+ // We're not interested in the main page, as we test that in other files.
+ // In this page we're only interested in the marker for requested resources.
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+
+ const parentFirstStopMarker = parentPairs[1][1];
+ const parentRedirectMarker = parentPairs[2][1];
+ const parentSecondStopMarker = parentPairs[3][1];
+ const contentFirstStopMarker = contentPairs[1][1];
+ const contentRedirectMarker = contentPairs[2][1];
+ const contentSecondStopMarker = contentPairs[3][1];
+
+ const expectedCommonDataProperties = {
+ type: "Network",
+ requestMethod: "GET",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ innerWindowID: Expect.number(),
+ };
+
+ // These properties are present when a connection is fully opened. This is
+ // most often the case, unless we're in verify mode, because in that case
+ // we run the same tests several times in the same Firefox and they might be
+ // cached, or in chaos mode Firefox may make all requests sequentially on
+ // the same connection.
+ // In these cases, these properties won't always be present.
+ const expectedConnectionProperties = {
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ };
+
+ const expectedPropertiesForStopMarker = {
+ name: Expect.stringMatches(/Load \d+:.*\/firefox-logo-nightly\.svg/),
+ };
+
+ const expectedDataPropertiesForStopMarker = {
+ ...expectedCommonDataProperties,
+ ...expectedConnectionProperties,
+ status: "STATUS_STOP",
+ URI: Expect.stringContains("/firefox-logo-nightly.svg"),
+ contentType: "image/svg+xml",
+ count: Expect.number(),
+ };
+
+ const expectedPropertiesForRedirectMarker = {
+ name: Expect.stringMatches(
+ /Load \d+:.*\/redirect.sjs\?firefox-logo-nightly\.svg/
+ ),
+ };
+
+ const expectedDataPropertiesForRedirectMarker = {
+ ...expectedCommonDataProperties,
+ ...expectedConnectionProperties,
+ status: "STATUS_REDIRECT",
+ URI: Expect.stringContains("/redirect.sjs?firefox-logo-nightly.svg"),
+ RedirectURI: Expect.stringContains("/firefox-logo-nightly.svg"),
+ contentType: null,
+ redirectType: "Permanent",
+ isHttpToHttpsRedirect: false,
+ };
+
+ Assert.objectContains(
+ parentFirstStopMarker,
+ expectedPropertiesForStopMarker
+ );
+ Assert.objectContainsOnly(parentFirstStopMarker.data, {
+ ...expectedDataPropertiesForStopMarker,
+ // The cache information is missing from the content marker, it's only part
+ // of the parent marker. See Bug 1544821.
+ // Also, because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+
+ Assert.objectContains(
+ contentFirstStopMarker,
+ expectedPropertiesForStopMarker
+ );
+ Assert.objectContainsOnly(
+ contentFirstStopMarker.data,
+ expectedDataPropertiesForStopMarker
+ );
+
+ Assert.objectContains(
+ parentRedirectMarker,
+ expectedPropertiesForRedirectMarker
+ );
+ Assert.objectContainsOnly(parentRedirectMarker.data, {
+ ...expectedDataPropertiesForRedirectMarker,
+ redirectId: parentSecondStopMarker.data.id,
+ // See above for the full explanation about the cache property.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+
+ Assert.objectContains(
+ contentRedirectMarker,
+ expectedPropertiesForRedirectMarker
+ );
+ Assert.objectContainsOnly(contentRedirectMarker.data, {
+ ...expectedDataPropertiesForRedirectMarker,
+ redirectId: contentSecondStopMarker.data.id,
+ });
+
+ Assert.objectContains(
+ parentSecondStopMarker,
+ expectedPropertiesForStopMarker
+ );
+ Assert.objectContainsOnly(parentSecondStopMarker.data, {
+ ...expectedDataPropertiesForStopMarker,
+ // The "count" property is absent from the content marker.
+ count: Expect.number(),
+ // See above for the full explanation about the cache property.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+
+ Assert.objectContains(
+ contentSecondStopMarker,
+ expectedPropertiesForStopMarker
+ );
+ Assert.objectContainsOnly(
+ contentSecondStopMarker.data,
+ expectedDataPropertiesForStopMarker
+ );
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_cache_first.js b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_cache_first.js
new file mode 100644
index 0000000000..4b715efa1c
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_cache_first.js
@@ -0,0 +1,388 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test a caching service worker. This service worker will
+ * fetch and store requests at install time, and serve them when the page
+ * requests them.
+ */
+
+const serviceWorkerFileName = "serviceworker_cache_first.js";
+registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData());
+
+add_task(async function test_network_markers_service_worker_setup() {
+ // Disabling cache makes the result more predictable. Also this makes things
+ // simpler when dealing with service workers.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_service_worker_register() {
+ // In this first step, we request an HTML page that will register a service
+ // worker. We'll wait until the service worker is fully installed before
+ // checking various things.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_register.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ await SpecialPowers.spawn(
+ contentBrowser,
+ [serviceWorkerFileName],
+ async function(serviceWorkerFileName) {
+ await content.wrappedJSObject.registerServiceWorkerAndWait(
+ serviceWorkerFileName
+ );
+ }
+ );
+
+ const {
+ parentThread,
+ contentThread,
+ profile,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+
+ // The service worker work happens in a third "thread" or process, let's try
+ // to find it.
+ // Currently the fetches happen on the main thread for the content process,
+ // this may change in the future and we may have to adapt this function.
+ // Also please note this isn't necessarily the same content process as the
+ // ones for the tab.
+ const { serviceWorkerParentThread } = findServiceWorkerThreads(profile);
+
+ // Here are a few sanity checks.
+ ok(
+ serviceWorkerParentThread,
+ "We should find a thread for the service worker."
+ );
+
+ Assert.notEqual(
+ serviceWorkerParentThread.pid,
+ parentThread.pid,
+ "We should have a different pid than the parent thread."
+ );
+ Assert.notEqual(
+ serviceWorkerParentThread.tid,
+ parentThread.tid,
+ "We should have a different tid than the parent thread."
+ );
+
+ // Let's make sure we actually have a registered service workers.
+ const workers = await SpecialPowers.registeredServiceWorkers();
+ Assert.equal(
+ workers.length,
+ 1,
+ "One service worker should be properly registered."
+ );
+
+ // By logging a few information about the threads we make debugging easier.
+ logInformationForThread("parentThread information", parentThread);
+ logInformationForThread("contentThread information", contentThread);
+ logInformationForThread(
+ "serviceWorkerParentThread information",
+ serviceWorkerParentThread
+ );
+
+ // Now let's check the marker payloads.
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread)
+ // When we load a page, Firefox will check the service worker freshness
+ // after a few seconds. So when the test lasts a long time (with some test
+ // environments) we might see spurious markers about that that we're not
+ // interesting in in this part of the test. They're only present in the
+ // parent process.
+ .filter(marker => !marker.data.URI.includes(serviceWorkerFileName));
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ const serviceWorkerNetworkMarkers = getInflatedNetworkMarkers(
+ serviceWorkerParentThread
+ );
+
+ // Some more logs for debugging purposes.
+ info(
+ "Parent network markers: " + JSON.stringify(parentNetworkMarkers, null, 2)
+ );
+ info(
+ "Content network markers: " +
+ JSON.stringify(contentNetworkMarkers, null, 2)
+ );
+ info(
+ "Serviceworker network markers: " +
+ JSON.stringify(serviceWorkerNetworkMarkers, null, 2)
+ );
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+ const serviceWorkerPairs = getPairsOfNetworkMarkers(
+ serviceWorkerNetworkMarkers
+ );
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+ serviceWorkerPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the service worker process.`
+ )
+ );
+
+ // Let's look at all pairs and make sure we requested all expected files.
+ const parentStopMarkers = parentPairs.map(([_, stopMarker]) => stopMarker);
+ const serviceWorkerStopMarkers = serviceWorkerPairs.map(
+ ([_, stopMarker]) => stopMarker
+ );
+
+ // These are the files cached by the service worker. We should see markers
+ // for both the parent thread and the service worker thread.
+ const expectedFiles = [
+ "serviceworker_page.html",
+ "firefox-logo-nightly.svg",
+ ].map(filename => `${BASE_URL_HTTPS}serviceworkers/${filename}`);
+
+ for (const expectedFile of expectedFiles) {
+ info(
+ `Checking if "${expectedFile}" is present in the network markers in both processes.`
+ );
+ const parentMarker = parentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+ const serviceWorkerMarker = serviceWorkerStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+
+ const expectedProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(expectedFile)}`
+ ),
+ data: Expect.objectContains({
+ status: "STATUS_STOP",
+ URI: expectedFile,
+ requestMethod: "GET",
+ contentType: Expect.stringMatches(/^(text\/html|image\/svg\+xml)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ }),
+ };
+
+ Assert.objectContains(parentMarker, expectedProperties);
+ Assert.objectContains(serviceWorkerMarker, expectedProperties);
+ }
+ });
+});
+
+add_task(async function test_network_markers_service_worker_use() {
+ // In this test we request an HTML file that itself contains resources that
+ // are redirected.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_page.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ // By logging a few information about the threads we make debugging easier.
+ logInformationForThread("parentThread information", parentThread);
+ logInformationForThread("contentThread information", contentThread);
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread)
+ // When we load a page, Firefox will check the service worker freshness
+ // after a few seconds. So when the test lasts a long time (with some test
+ // environments) we might see spurious markers about that that we're not
+ // interesting in in this part of the test. They're only present in the
+ // parent process.
+ .filter(marker => !marker.data.URI.includes(serviceWorkerFileName));
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+
+ // Here are some logs to ease debugging.
+ info(
+ "Parent network markers: " + JSON.stringify(parentNetworkMarkers, null, 2)
+ );
+ info(
+ "Content network markers: " +
+ JSON.stringify(contentNetworkMarkers, null, 2)
+ );
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+
+ // These are the files cached by the service worker. We should see markers
+ // for the parent thread and the content thread.
+ const expectedFiles = [
+ "serviceworker_page.html",
+ "firefox-logo-nightly.svg",
+ ].map(filename => `${BASE_URL_HTTPS}serviceworkers/${filename}`);
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+
+ // Let's look at all pairs and make sure we requested all expected files.
+ const parentEndMarkers = parentPairs.map(([_, endMarker]) => endMarker);
+ const contentStopMarkers = contentPairs.map(
+ ([_, stopMarker]) => stopMarker
+ );
+
+ Assert.equal(
+ parentEndMarkers.length,
+ expectedFiles.length * 2, // one redirect + one stop
+ "There should be twice as many end markers in the parent process as requested files."
+ );
+ Assert.equal(
+ contentStopMarkers.length,
+ expectedFiles.length,
+ "There should be as many stop markers in the content process as requested files."
+ );
+
+ for (const [i, expectedFile] of expectedFiles.entries()) {
+ info(
+ `Checking if "${expectedFile}" if present in the network markers in both processes.`
+ );
+ const [parentRedirectMarker, parentStopMarker] = parentEndMarkers.filter(
+ marker => marker.data.URI === expectedFile
+ );
+ const contentMarker = contentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+
+ const commonDataProperties = {
+ type: "Network",
+ URI: expectedFile,
+ requestMethod: "GET",
+ contentType: Expect.stringMatches(/^(text\/html|image\/svg\+xml)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ };
+
+ const expectedProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(expectedFile)}`
+ ),
+ };
+
+ Assert.objectContains(parentRedirectMarker, expectedProperties);
+ Assert.objectContains(parentStopMarker, expectedProperties);
+ Assert.objectContains(contentMarker, expectedProperties);
+ if (i === 0) {
+ // This is the top level navigation, the HTML file.
+ Assert.objectContainsOnly(parentRedirectMarker.data, {
+ ...commonDataProperties,
+ status: "STATUS_REDIRECT",
+ contentType: null,
+ cache: "Unresolved",
+ RedirectURI: expectedFile,
+ redirectType: "Internal",
+ redirectId: parentStopMarker.data.id,
+ isHttpToHttpsRedirect: false,
+ });
+
+ Assert.objectContainsOnly(parentStopMarker.data, {
+ ...commonDataProperties,
+ status: "STATUS_STOP",
+ });
+
+ Assert.objectContainsOnly(contentMarker.data, {
+ ...commonDataProperties,
+ status: "STATUS_STOP",
+ });
+ } else {
+ Assert.objectContainsOnly(parentRedirectMarker.data, {
+ ...commonDataProperties,
+ status: "STATUS_REDIRECT",
+ contentType: null,
+ cache: "Unresolved",
+ innerWindowID: Expect.number(),
+ RedirectURI: expectedFile,
+ redirectType: "Internal",
+ redirectId: parentStopMarker.data.id,
+ isHttpToHttpsRedirect: false,
+ });
+
+ Assert.objectContainsOnly(
+ parentStopMarker.data,
+ // Note: in the future we may have more properties. We're using the
+ // "Only" flavor of the matcher so that we don't forget to update this
+ // test when this changes.
+ {
+ ...commonDataProperties,
+ innerWindowID: Expect.number(),
+ status: "STATUS_STOP",
+ }
+ );
+
+ Assert.objectContainsOnly(contentMarker.data, {
+ ...commonDataProperties,
+ innerWindowID: Expect.number(),
+ status: "STATUS_STOP",
+ });
+ }
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_fetch_handler.js b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_fetch_handler.js
new file mode 100644
index 0000000000..775e81f565
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_fetch_handler.js
@@ -0,0 +1,218 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test the case of a service worker that has no fetch
+ * handlers. In this case, a fetch is done to the network. There may be
+ * shortcuts in our code in this case, that's why it's important to test it
+ * separately.
+ */
+
+const serviceWorkerFileName = "serviceworker_no_fetch_handler.js";
+registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData());
+
+add_task(async function test_network_markers_service_worker_setup() {
+ // Disabling cache makes the result more predictable. Also this makes things
+ // simpler when dealing with service workers.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_service_worker_register() {
+ // In this first step, we request an HTML page that will register a service
+ // worker. We'll wait until the service worker is fully installed before
+ // checking various things.
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_register.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ await SpecialPowers.spawn(
+ contentBrowser,
+ [serviceWorkerFileName],
+ async function(serviceWorkerFileName) {
+ await content.wrappedJSObject.registerServiceWorkerAndWait(
+ serviceWorkerFileName
+ );
+ }
+ );
+
+ // Let's make sure we actually have a registered service workers.
+ const workers = await SpecialPowers.registeredServiceWorkers();
+ Assert.equal(
+ workers.length,
+ 1,
+ "One service worker should be properly registered."
+ );
+ });
+});
+
+add_task(async function test_network_markers_service_worker_use() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_page.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ // By logging a few information about the threads we make debugging easier.
+ logInformationForThread("parentThread information", parentThread);
+ logInformationForThread("contentThread information", contentThread);
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread)
+ // When we load a page, Firefox will check the service worker freshness
+ // after a few seconds. So when the test lasts a long time (with some test
+ // environments) we might see spurious markers about that that we're not
+ // interesting in in this part of the test. They're only present in the
+ // parent process.
+ .filter(marker => !marker.data.URI.includes(serviceWorkerFileName));
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+
+ // Here are some logs to ease debugging.
+ info(
+ "Parent network markers:" + JSON.stringify(parentNetworkMarkers, null, 2)
+ );
+ info(
+ "Content network markers:" +
+ JSON.stringify(contentNetworkMarkers, null, 2)
+ );
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+
+ // Let's look at all pairs and make sure we requested all expected files.
+ const parentStopMarkers = parentPairs.map(([_, stopMarker]) => stopMarker);
+ const contentStopMarkers = contentPairs.map(
+ ([_, stopMarker]) => stopMarker
+ );
+
+ // These are the files requested by the page.
+ // We should see markers for the parent thread and the content thread.
+ const expectedFiles = [
+ // Please take care that the first element is the top level navigation, as
+ // this is special-cased below.
+ "serviceworker_page.html",
+ "firefox-logo-nightly.svg",
+ ].map(filename => `${BASE_URL_HTTPS}serviceworkers/${filename}`);
+
+ Assert.equal(
+ parentStopMarkers.length,
+ expectedFiles.length,
+ "There should be as many stop markers in the parent process as requested files."
+ );
+ Assert.equal(
+ contentStopMarkers.length,
+ expectedFiles.length,
+ "There should be as many stop markers in the content process as requested files."
+ );
+
+ for (const [i, expectedFile] of expectedFiles.entries()) {
+ info(
+ `Checking if "${expectedFile}" if present in the network markers in both processes.`
+ );
+ const parentMarker = parentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+ const contentMarker = contentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+
+ const commonProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(expectedFile)}`
+ ),
+ };
+ Assert.objectContains(parentMarker, commonProperties);
+ Assert.objectContains(contentMarker, commonProperties);
+
+ // We get the full set of properties in this case, because we do an actual
+ // fetch to the network.
+ const commonDataProperties = {
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: expectedFile,
+ requestMethod: "GET",
+ contentType: Expect.stringMatches(/^(text\/html|image\/svg\+xml)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ count: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ };
+
+ if (i === 0) {
+ // The first marker is special cased: this is the top level navigation
+ // serviceworker_page.html,
+ // and in this case we don't have all the same properties. Especially
+ // the innerWindowID information is missing.
+ Assert.objectContainsOnly(parentMarker.data, {
+ ...commonDataProperties,
+ // Note that the parent process has the "cache" information, but not the content
+ // process. See Bug 1544821.
+ // Also because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+
+ Assert.objectContainsOnly(contentMarker.data, commonDataProperties);
+ } else {
+ // This is the other file firefox-logo-nightly.svg.
+ Assert.objectContainsOnly(parentMarker.data, {
+ ...commonDataProperties,
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ innerWindowID: Expect.number(),
+ });
+
+ Assert.objectContainsOnly(contentMarker.data, {
+ ...commonDataProperties,
+ innerWindowID: Expect.number(),
+ });
+ }
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_respondWith_in_fetch_handler.js b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_respondWith_in_fetch_handler.js
new file mode 100644
index 0000000000..65686257de
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_no_respondWith_in_fetch_handler.js
@@ -0,0 +1,296 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test the case of a service worker that has a fetch
+ * handler, but no respondWith. In this case, some process called "reset
+ * interception" happens, and the fetch is still carried on by our code. Because
+ * this is a bit of an edge case, it's important to have a test for this case.
+ */
+
+const serviceWorkerFileName =
+ "serviceworker_no_respondWith_in_fetch_handler.js";
+registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData());
+
+add_task(async function test_network_markers_service_worker_setup() {
+ // Disabling cache makes the result more predictable. Also this makes things
+ // simpler when dealing with service workers.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_service_worker_register() {
+ // In this first step, we request an HTML page that will register a service
+ // worker. We'll wait until the service worker is fully installed before
+ // checking various things.
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_register.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ await SpecialPowers.spawn(
+ contentBrowser,
+ [serviceWorkerFileName],
+ async function(serviceWorkerFileName) {
+ await content.wrappedJSObject.registerServiceWorkerAndWait(
+ serviceWorkerFileName
+ );
+ }
+ );
+
+ // Let's make sure we actually have a registered service workers.
+ const workers = await SpecialPowers.registeredServiceWorkers();
+ Assert.equal(
+ workers.length,
+ 1,
+ "One service worker should be properly registered."
+ );
+ });
+});
+
+add_task(async function test_network_markers_service_worker_use() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_page.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ // By logging a few information about the threads we make debugging easier.
+ logInformationForThread("parentThread information", parentThread);
+ logInformationForThread("contentThread information", contentThread);
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread)
+ // When we load a page, Firefox will check the service worker freshness
+ // after a few seconds. So when the test lasts a long time (with some test
+ // environments) we might see spurious markers about that that we're not
+ // interesting in in this part of the test. They're only present in the
+ // parent process.
+ .filter(marker => !marker.data.URI.includes(serviceWorkerFileName));
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+
+ // Here are some logs to ease debugging.
+ info(
+ "Parent network markers:" + JSON.stringify(parentNetworkMarkers, null, 2)
+ );
+ info(
+ "Content network markers:" +
+ JSON.stringify(contentNetworkMarkers, null, 2)
+ );
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+
+ // Let's look at all pairs and make sure we requested all expected files.
+ // In this test, we should have redirect markers as well as stop markers,
+ // because this case generates internal redirects. We may want to change
+ // that in the future, or handle this specially in the frontend.
+ // Let's create various arrays to help assert.
+
+ const parentEndMarkers = parentPairs.map(([_, stopMarker]) => stopMarker);
+ const parentStopMarkers = parentEndMarkers.filter(
+ marker => marker.data.status === "STATUS_STOP"
+ );
+ const parentRedirectMarkers = parentEndMarkers.filter(
+ marker => marker.data.status === "STATUS_REDIRECT"
+ );
+ const contentEndMarkers = contentPairs.map(([_, stopMarker]) => stopMarker);
+ const contentStopMarkers = contentEndMarkers.filter(
+ marker => marker.data.status === "STATUS_STOP"
+ );
+ const contentRedirectMarkers = contentEndMarkers.filter(
+ marker => marker.data.status === "STATUS_REDIRECT"
+ );
+
+ // These are the files requested by the page.
+ // We should see markers for the parent thread and the content thread.
+ const expectedFiles = [
+ // Please take care that the first element is the top level navigation, as
+ // this is special-cased below.
+ "serviceworker_page.html",
+ "firefox-logo-nightly.svg",
+ ].map(filename => `${BASE_URL_HTTPS}serviceworkers/${filename}`);
+
+ Assert.equal(
+ parentStopMarkers.length,
+ expectedFiles.length,
+ "There should be as many stop markers in the parent process as requested files."
+ );
+ Assert.equal(
+ parentRedirectMarkers.length,
+ expectedFiles.length * 2, // http -> intercepted, intercepted -> http
+ "There should be twice as many redirect markers in the parent process as requested files."
+ );
+ Assert.equal(
+ contentStopMarkers.length,
+ expectedFiles.length,
+ "There should be as many stop markers in the content process as requested files."
+ );
+ // Note: there will no redirect markers in the content process for
+ // ServiceWorker fallbacks request to network.
+ // See Bug 1793940.
+ Assert.equal(
+ contentRedirectMarkers.length,
+ 0,
+ "There should be no redirect markers in the content process than requested files."
+ );
+
+ for (const [i, expectedFile] of expectedFiles.entries()) {
+ info(
+ `Checking if "${expectedFile}" if present in the network markers in both processes.`
+ );
+ const [
+ parentRedirectMarkerIntercept,
+ parentRedirectMarkerReset,
+ ] = parentRedirectMarkers.filter(
+ marker => marker.data.URI === expectedFile
+ );
+ const parentStopMarker = parentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+ const contentStopMarker = contentStopMarkers.find(
+ marker => marker.data.URI === expectedFile
+ );
+
+ const commonProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(expectedFile)}`
+ ),
+ };
+ Assert.objectContains(parentRedirectMarkerIntercept, commonProperties);
+ Assert.objectContains(parentRedirectMarkerReset, commonProperties);
+ Assert.objectContains(parentStopMarker, commonProperties);
+ Assert.objectContains(contentStopMarker, commonProperties);
+ // Note: there's no check for the contentRedirectMarker, because there's
+ // no marker for a top level navigation redirect in the content process.
+
+ // We get the full set of properties in this case, because we do an actual
+ // fetch to the network.
+ const commonDataProperties = {
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: expectedFile,
+ requestMethod: "GET",
+ contentType: Expect.stringMatches(/^(text\/html|image\/svg\+xml)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ count: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ };
+
+ const commonRedirectProperties = {
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: expectedFile,
+ RedirectURI: expectedFile,
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ redirectType: "Internal",
+ isHttpToHttpsRedirect: false,
+ };
+
+ if (i === 0) {
+ // The first marker is special cased: this is the top level navigation
+ // serviceworker_page.html,
+ // and in this case we don't have all the same properties. Especially
+ // the innerWindowID information is missing.
+ Assert.objectContainsOnly(parentStopMarker.data, {
+ ...commonDataProperties,
+ // Note that the parent process has the "cache" information, but not the content
+ // process. See Bug 1544821.
+ // Also, because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+ Assert.objectContainsOnly(contentStopMarker.data, commonDataProperties);
+
+ Assert.objectContainsOnly(parentRedirectMarkerIntercept.data, {
+ ...commonRedirectProperties,
+ redirectId: parentRedirectMarkerReset.data.id,
+ cache: "Unresolved",
+ });
+ Assert.objectContainsOnly(parentRedirectMarkerReset.data, {
+ ...commonRedirectProperties,
+ redirectId: parentStopMarker.data.id,
+ });
+
+ // Note: there's no check for the contentRedirectMarker, because there's
+ // no marker for a top level navigation redirect in the content process.
+ } else {
+ // This is the other file firefox-logo-nightly.svg.
+ Assert.objectContainsOnly(parentStopMarker.data, {
+ ...commonDataProperties,
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ innerWindowID: Expect.number(),
+ });
+ Assert.objectContains(contentStopMarker, commonProperties);
+ Assert.objectContainsOnly(contentStopMarker.data, {
+ ...commonDataProperties,
+ innerWindowID: Expect.number(),
+ });
+
+ Assert.objectContainsOnly(parentRedirectMarkerIntercept.data, {
+ ...commonRedirectProperties,
+ innerWindowID: Expect.number(),
+ redirectId: parentRedirectMarkerReset.data.id,
+ cache: "Unresolved",
+ });
+ Assert.objectContainsOnly(parentRedirectMarkerReset.data, {
+ ...commonRedirectProperties,
+ innerWindowID: Expect.number(),
+ redirectId: parentStopMarker.data.id,
+ });
+ }
+ }
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_synthetized_response.js b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_synthetized_response.js
new file mode 100644
index 0000000000..41690e11a6
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_serviceworker_synthetized_response.js
@@ -0,0 +1,558 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test a service worker that returns a synthetized response.
+ * This means the service worker will make up a response by itself.
+ */
+
+const serviceWorkerFileName = "serviceworker_synthetized_response.js";
+registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData());
+
+add_task(async function test_network_markers_service_worker_setup() {
+ // Disabling cache makes the result more predictable. Also this makes things
+ // simpler when dealing with service workers.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_service_worker_register() {
+ // In this first step, we request an HTML page that will register a service
+ // worker. We'll wait until the service worker is fully installed before
+ // checking various things.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_register.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ await SpecialPowers.spawn(
+ contentBrowser,
+ [serviceWorkerFileName],
+ async function(serviceWorkerFileName) {
+ await content.wrappedJSObject.registerServiceWorkerAndWait(
+ serviceWorkerFileName
+ );
+ }
+ );
+
+ // Let's make sure we actually have a registered service workers.
+ const workers = await SpecialPowers.registeredServiceWorkers();
+ Assert.equal(
+ workers.length,
+ 1,
+ "One service worker should be properly registered."
+ );
+ });
+});
+
+add_task(async function test_network_markers_service_worker_use() {
+ // In this test, we'll first load a plain html file, then do some fetch
+ // requests in the context of the page. One request is served with a
+ // synthetized response, the other request is served with a real "fetch" done
+ // by the service worker.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = `${BASE_URL_HTTPS}serviceworkers/serviceworker_simple.html`;
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ await SpecialPowers.spawn(contentBrowser, [], async () => {
+ // This request is served directly by the service worker as a synthetized response.
+ await content
+ .fetch("firefox-generated.svg")
+ .then(res => res.arrayBuffer());
+
+ // This request is served by a fetch done inside the service worker.
+ await content
+ .fetch("firefox-logo-nightly.svg")
+ .then(res => res.arrayBuffer());
+ });
+
+ const {
+ parentThread,
+ contentThread,
+ profile,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+
+ // The service worker work happens in a third "thread" or process, let's try
+ // to find it.
+ // Currently the fetches happen on the main thread for the content process,
+ // this may change in the future and we may have to adapt this function.
+ // Also please note this isn't necessarily the same content process as the
+ // ones for the tab.
+ const { serviceWorkerParentThread } = findServiceWorkerThreads(profile);
+
+ ok(
+ serviceWorkerParentThread,
+ "We should find a thread for the service worker."
+ );
+
+ // By logging a few information about the threads we make debugging easier.
+ logInformationForThread("parentThread information", parentThread);
+ logInformationForThread("contentThread information", contentThread);
+ logInformationForThread(
+ "serviceWorkerParentThread information",
+ serviceWorkerParentThread
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread)
+ // When we load a page, Firefox will check the service worker freshness
+ // after a few seconds. So when the test lasts a long time (with some test
+ // environments) we might see spurious markers about that that we're not
+ // interesting in in this part of the test. They're only present in the
+ // parent process.
+ .filter(marker => !marker.data.URI.includes(serviceWorkerFileName));
+
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ const serviceWorkerNetworkMarkers = getInflatedNetworkMarkers(
+ serviceWorkerParentThread
+ );
+
+ // Some more logs for debugging purposes.
+ info(
+ "Parent network markers: " + JSON.stringify(parentNetworkMarkers, null, 2)
+ );
+ info(
+ "Content network markers: " +
+ JSON.stringify(contentNetworkMarkers, null, 2)
+ );
+ info(
+ "Serviceworker network markers: " +
+ JSON.stringify(serviceWorkerNetworkMarkers, null, 2)
+ );
+
+ const parentPairs = getPairsOfNetworkMarkers(parentNetworkMarkers);
+ const contentPairs = getPairsOfNetworkMarkers(contentNetworkMarkers);
+ const serviceWorkerPairs = getPairsOfNetworkMarkers(
+ serviceWorkerNetworkMarkers
+ );
+
+ // First, make sure we properly matched all start with stop markers. This
+ // means that both arrays should contain only arrays of 2 elements.
+ parentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the parent process.`
+ )
+ );
+
+ contentPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the content process.`
+ )
+ );
+ serviceWorkerPairs.forEach(pair =>
+ Assert.equal(
+ pair.length,
+ 2,
+ `For the URL ${pair[0].data.URI} we should get 2 markers in the service worker process.`
+ )
+ );
+
+ // Let's look at all pairs and make sure we requested all expected files.
+ // In this test, we should have redirect markers as well as stop markers,
+ // because this case generates internal redirects.
+ // Let's create various arrays to help assert.
+
+ let parentStopMarkers = parentPairs.map(([_, stopMarker]) => stopMarker);
+ const contentStopMarkers = contentPairs.map(
+ ([_, stopMarker]) => stopMarker
+ );
+ const serviceWorkerStopMarkers = serviceWorkerPairs.map(
+ ([_, stopMarker]) => stopMarker
+ );
+
+ // In this test we have very different results in the various threads, so
+ // we'll assert every case separately.
+ // A simple function to help constructing better assertions:
+ const fullUrl = filename => `${BASE_URL_HTTPS}serviceworkers/${filename}`;
+
+ {
+ // In the parent process, we have 8 network markers:
+ // - twice the html file -- because it's not cached by the SW, we get the
+ // marker both for the initial request and for the request initied from the
+ // SW.
+ // - twice the firefox svg file -- similar situation
+ // - once the generated svg file -- this one isn't fetched by the SW but
+ // rather forged directly, so there's no "second fetch", and thus we have
+ // only one marker.
+ // - for each of these files, we have first an internal redirect from the
+ // main channel to the service worker. => 3 redirect markers more.
+ Assert.equal(
+ parentStopMarkers.length,
+ 8, // 3 html files, 3 firefox svg files, 2 generated svg file
+ "There should be 8 stop markers in the parent process."
+ );
+
+ // The "1" requests are the initial requests that are intercepted, coming
+ // from the web page, while the "2" requests are requests to the network,
+ // coming from the service worker. The 1 were requested before 2, 2 ends
+ // before 1.
+ // "Intercept" requests are the internal redirects from the main channel
+ // to the service worker. They happen before others.
+ const [
+ htmlFetchIntercept,
+ htmlFetch1,
+ htmlFetch2,
+ generatedSvgIntercept,
+ generatedSvgFetch,
+ firefoxSvgIntercept,
+ firefoxSvgFetch1,
+ firefoxSvgFetch2,
+ ] = parentStopMarkers;
+
+ /* ----- /HTML FILE ---- */
+ Assert.objectContains(htmlFetchIntercept, {
+ name: Expect.stringMatches(/Load \d+:.*serviceworker_simple.html/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: fullUrl("serviceworker_simple.html"),
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ redirectId: htmlFetch1.data.id,
+ redirectType: "Internal",
+ isHttpToHttpsRedirect: false,
+ RedirectURI: fullUrl("serviceworker_simple.html"),
+ cache: "Unresolved",
+ }),
+ });
+
+ Assert.objectContains(htmlFetch1, {
+ name: Expect.stringMatches(/Load \d+:.*serviceworker_simple.html/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("serviceworker_simple.html"),
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ }),
+ });
+ Assert.objectContains(htmlFetch2, {
+ name: Expect.stringMatches(/Load \d+:.*serviceworker_simple.html/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("serviceworker_simple.html"),
+ requestMethod: "GET",
+ contentType: "text/html",
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ }),
+ });
+ /* ----- /HTML FILE ---- */
+
+ /* ----- GENERATED SVG FILE ---- */
+ Assert.objectContains(generatedSvgIntercept, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-generated.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: fullUrl("firefox-generated.svg"),
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ redirectId: generatedSvgFetch.data.id,
+ redirectType: "Internal",
+ isHttpToHttpsRedirect: false,
+ RedirectURI: fullUrl("firefox-generated.svg"),
+ cache: "Unresolved",
+ innerWindowID: Expect.number(),
+ }),
+ });
+ Assert.objectContains(generatedSvgFetch, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-generated.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-generated.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ innerWindowID: Expect.number(),
+ }),
+ });
+ /* ----- ∕GENERATED SVG FILE ---- */
+ /* ----- REQUESTED SVG FILE ---- */
+ Assert.objectContains(firefoxSvgIntercept, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-logo-nightly.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: fullUrl("firefox-logo-nightly.svg"),
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ redirectId: firefoxSvgFetch1.data.id,
+ redirectType: "Internal",
+ isHttpToHttpsRedirect: false,
+ RedirectURI: fullUrl("firefox-logo-nightly.svg"),
+ cache: "Unresolved",
+ innerWindowID: Expect.number(),
+ }),
+ });
+ Assert.objectContains(firefoxSvgFetch1, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-logo-nightly.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-logo-nightly.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ innerWindowID: Expect.number(),
+ }),
+ });
+ Assert.objectContains(firefoxSvgFetch2, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-logo-nightly.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-logo-nightly.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ // Note: no innerWindowID here, is that a bug?
+ }),
+ });
+ /* ----- ∕REQUESTED SVG FILE ---- */
+ }
+
+ // It's possible that the service worker thread IS the content thread, in
+ // that case we'll get all markers in the same thread.
+ // The "1" requests are the initial requests that are intercepted, coming
+ // from the web page, while the "2" requests are the requests coming from
+ // the service worker.
+ let htmlFetch1,
+ htmlFetch2,
+ generatedSvgFetch1,
+ firefoxSvgFetch1,
+ firefoxSvgFetch2;
+
+ // First, let's handle the case where the threads are different:
+ if (serviceWorkerParentThread !== contentThread) {
+ // In the content process (that is the process for the web page), we have
+ // 3 network markers:
+ // - 1 for the HTML page
+ // - 1 for the generated svg file
+ // - 1 for the firefox svg file
+ // Indeed, the service worker interception is invisible from the context
+ // of the web page, so we just get 3 "normal" requests. However these
+ // requests will miss all timing information, because they're hidden by
+ // the service worker interception. We may want to fix this...
+ Assert.equal(
+ contentStopMarkers.length,
+ 3, // 1 for each file
+ "There should be 3 stop markers in the content process."
+ );
+
+ [htmlFetch1, generatedSvgFetch1, firefoxSvgFetch1] = contentStopMarkers;
+
+ // In the service worker parent thread, we have 2 network markers:
+ // - the HTML file
+ // - the firefox SVG file.
+ // Remember that the generated SVG file is returned directly by the SW.
+ Assert.equal(
+ serviceWorkerStopMarkers.length,
+ 2,
+ "There should be 2 stop markers in the service worker thread."
+ );
+
+ [htmlFetch2, firefoxSvgFetch2] = serviceWorkerStopMarkers;
+ } else {
+ // Else case: the service worker parent thread IS the content thread
+ // (note: this is always the case with fission). In that case all network
+ // markers tested in the above block are together in the same object.
+ Assert.equal(
+ contentStopMarkers.length,
+ 5,
+ "There should be 5 stop markers in the combined process (containing both the content page and the service worker)"
+ );
+
+ // Because of how the test is done, these markers are ordered by the
+ // position of the START markers.
+ [
+ // For the htmlFetch request, note that 2 is before 1, because that's
+ // the top level navigation. Indeed for the top level navigation
+ // everything happens first in the main process, possibly before a
+ // content process even exists, and the content process is merely
+ // notified at the end.
+ htmlFetch2,
+ htmlFetch1,
+ generatedSvgFetch1,
+ firefoxSvgFetch1,
+ firefoxSvgFetch2,
+ ] = contentStopMarkers;
+ }
+
+ // Let's test first the markers coming from the content page.
+ Assert.objectContains(htmlFetch1, {
+ name: Expect.stringMatches(/Load \d+:.*serviceworker_simple.html/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("serviceworker_simple.html"),
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ }),
+ });
+ Assert.objectContains(generatedSvgFetch1, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-generated.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-generated.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ innerWindowID: Expect.number(),
+ }),
+ });
+ Assert.objectContains(firefoxSvgFetch1, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-logo-nightly.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-logo-nightly.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ pri: Expect.number(),
+ innerWindowID: Expect.number(),
+ }),
+ });
+
+ // Now let's test the markers coming from the service worker.
+ Assert.objectContains(htmlFetch2, {
+ name: Expect.stringMatches(/Load \d+:.*serviceworker_simple.html/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("serviceworker_simple.html"),
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ // Note: no innerWindowID here, is that a bug?
+ // Note: no cache either, this is bug 1544821.
+ }),
+ });
+
+ Assert.objectContains(firefoxSvgFetch2, {
+ name: Expect.stringMatches(/Load \d+:.*firefox-logo-nightly.svg/),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: fullUrl("firefox-logo-nightly.svg"),
+ requestMethod: "GET",
+ contentType: "image/svg+xml",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ // Note: no innerWindowID here, is that a bug?
+ // Note: no cache either, this is bug 1544821.
+ }),
+ });
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_simple.js b/tools/profiler/tests/browser/browser_test_marker_network_simple.js
new file mode 100644
index 0000000000..15894305a7
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_simple.js
@@ -0,0 +1,81 @@
+/* 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 emit network markers accordingly
+ */
+add_task(async function test_network_markers() {
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = BASE_URL + "simple.html?cacheBust=" + Math.random();
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ info(JSON.stringify(parentNetworkMarkers, null, 2));
+ info(JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 2,
+ `We should get a pair of network markers in the parent thread.`
+ );
+ Assert.equal(
+ contentNetworkMarkers.length,
+ 2,
+ `We should get a pair of network markers in the content thread.`
+ );
+
+ const parentStopMarker = parentNetworkMarkers[1];
+ const contentStopMarker = contentNetworkMarkers[1];
+
+ const expectedProperties = {
+ name: Expect.stringMatches(`Load \\d+:.*${escapeStringRegexp(url)}`),
+ data: Expect.objectContains({
+ status: "STATUS_STOP",
+ URI: url,
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ }),
+ };
+
+ Assert.objectContains(parentStopMarker, expectedProperties);
+ // The cache information is missing from the content marker, it's only part
+ // of the parent marker. See Bug 1544821.
+ Assert.objectContains(parentStopMarker.data, {
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+ Assert.objectContains(contentStopMarker, expectedProperties);
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_marker_network_sts.js b/tools/profiler/tests/browser/browser_test_marker_network_sts.js
new file mode 100644
index 0000000000..26f2a1c756
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_marker_network_sts.js
@@ -0,0 +1,130 @@
+/* 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 emit network markers accordingly.
+ * In this file we'll test that we behave properly with STS redirections.
+ */
+
+add_task(async function test_network_markers_service_worker_setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Disabling cache makes the result more predictable especially in verify mode.
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ // We want to test upgrading requests
+ ["dom.security.https_only_mode", true],
+ ],
+ });
+});
+
+add_task(async function test_network_markers_redirect_to_https() {
+ // In this test, we request an HTML page with http that gets redirected to https.
+ Assert.ok(
+ !Services.profiler.IsActive(),
+ "The profiler is not currently active"
+ );
+
+ startProfilerForMarkerTests();
+
+ const url = BASE_URL + "simple.html";
+ const targetUrl = BASE_URL_HTTPS + "simple.html";
+
+ await BrowserTestUtils.withNewTab(url, async contentBrowser => {
+ const contentPid = await SpecialPowers.spawn(
+ contentBrowser,
+ [],
+ () => Services.appinfo.processID
+ );
+
+ const { parentThread, contentThread } = await stopProfilerNowAndGetThreads(
+ contentPid
+ );
+
+ const parentNetworkMarkers = getInflatedNetworkMarkers(parentThread);
+ const contentNetworkMarkers = getInflatedNetworkMarkers(contentThread);
+ info(JSON.stringify(parentNetworkMarkers, null, 2));
+ info(JSON.stringify(contentNetworkMarkers, null, 2));
+
+ Assert.equal(
+ parentNetworkMarkers.length,
+ 4,
+ `We should get 2 pairs of network markers in the parent thread.`
+ );
+
+ /* It looks like that for a redirection for the top level navigation, the
+ * content thread sees the markers for the second request only.
+ * See Bug 1692879. */
+ Assert.equal(
+ contentNetworkMarkers.length,
+ 2,
+ `We should get one pair of network markers in the content thread.`
+ );
+
+ const parentRedirectMarker = parentNetworkMarkers[1];
+ const parentStopMarker = parentNetworkMarkers[3];
+ // There's no content redirect marker for the reason outlined above.
+ const contentStopMarker = contentNetworkMarkers[1];
+
+ Assert.objectContains(parentRedirectMarker, {
+ name: Expect.stringMatches(`Load \\d+:.*${escapeStringRegexp(url)}`),
+ data: Expect.objectContainsOnly({
+ type: "Network",
+ status: "STATUS_REDIRECT",
+ URI: url,
+ RedirectURI: targetUrl,
+ requestMethod: "GET",
+ contentType: null,
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ id: Expect.number(),
+ redirectId: parentStopMarker.data.id,
+ pri: Expect.number(),
+ cache: "Unresolved",
+ redirectType: "Permanent",
+ isHttpToHttpsRedirect: true,
+ }),
+ });
+
+ const expectedProperties = {
+ name: Expect.stringMatches(
+ `Load \\d+:.*${escapeStringRegexp(targetUrl)}`
+ ),
+ };
+ const expectedDataProperties = {
+ type: "Network",
+ status: "STATUS_STOP",
+ URI: targetUrl,
+ requestMethod: "GET",
+ contentType: "text/html",
+ startTime: Expect.number(),
+ endTime: Expect.number(),
+ domainLookupStart: Expect.number(),
+ domainLookupEnd: Expect.number(),
+ connectStart: Expect.number(),
+ tcpConnectEnd: Expect.number(),
+ connectEnd: Expect.number(),
+ requestStart: Expect.number(),
+ responseStart: Expect.number(),
+ responseEnd: Expect.number(),
+ id: Expect.number(),
+ count: Expect.number(),
+ pri: Expect.number(),
+ };
+
+ Assert.objectContains(parentStopMarker, expectedProperties);
+ Assert.objectContains(contentStopMarker, expectedProperties);
+
+ // The cache information is missing from the content marker, it's only part
+ // of the parent marker. See Bug 1544821.
+ Assert.objectContainsOnly(parentStopMarker.data, {
+ ...expectedDataProperties,
+ // Because the request races with the cache, these 2 values are valid:
+ // "Missed" when the cache answered before we get a result from the network.
+ // "Unresolved" when we got a response from the network before the cache subsystem.
+ cache: Expect.stringMatches(/^(Missed|Unresolved)$/),
+ });
+ Assert.objectContainsOnly(contentStopMarker.data, expectedDataProperties);
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_markers_gc_cc.js b/tools/profiler/tests/browser/browser_test_markers_gc_cc.js
new file mode 100644
index 0000000000..a4a94d60cc
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_markers_gc_cc.js
@@ -0,0 +1,49 @@
+/* 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 function test_markers_gc_cc() {
+ info("Test GC&CC markers.");
+
+ info("Create a throwaway profile.");
+ await startProfiler({});
+ let tempProfileContainer = { profile: null };
+ tempProfileContainer.profile = await waitSamplingAndStopAndGetProfile();
+
+ info("Restart the profiler.");
+ await startProfiler({});
+
+ info("Throw away the previous profile, which should be garbage-collected.");
+ Assert.equal(
+ typeof tempProfileContainer.profile,
+ "object",
+ "Previously-captured profile should be an object"
+ );
+ delete tempProfileContainer.profile;
+ Assert.equal(
+ typeof tempProfileContainer.profile,
+ "undefined",
+ "Deleted profile should now be undefined"
+ );
+
+ info("Force GC&CC");
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+
+ info("Stop the profiler and get the profile.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+
+ const markers = getInflatedMarkerData(profile.threads[0]);
+ Assert.ok(
+ markers.some(({ data }) => data?.type === "GCSlice"),
+ "A GCSlice marker was recorded"
+ );
+ Assert.ok(
+ markers.some(({ data }) => data?.type === "CCSlice"),
+ "A CCSlice marker was recorded"
+ );
+});
diff --git a/tools/profiler/tests/browser/browser_test_markers_parent_process.js b/tools/profiler/tests/browser/browser_test_markers_parent_process.js
new file mode 100644
index 0000000000..28b82f8054
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_markers_parent_process.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/. */
+
+add_task(async function test_markers_parent_process() {
+ info("Test markers that are generated by the browser's parent process.");
+
+ info("Start the profiler in nostacksampling mode.");
+ await startProfiler({ features: ["nostacksampling"] });
+
+ info("Dispatch a DOMEvent");
+ window.dispatchEvent(new Event("synthetic"));
+
+ info("Stop the profiler and get the profile.");
+ const profile = await stopNowAndGetProfile();
+
+ const markers = getInflatedMarkerData(profile.threads[0]);
+ {
+ const domEventStart = markers.find(
+ ({ phase, data }) =>
+ phase === INTERVAL_START && data?.eventType === "synthetic"
+ );
+ const domEventEnd = markers.find(
+ ({ phase, data }) =>
+ phase === INTERVAL_END && data?.eventType === "synthetic"
+ );
+ ok(domEventStart, "A start DOMEvent was generated");
+ ok(domEventEnd, "An end DOMEvent was generated");
+ ok(
+ domEventEnd.data.latency > 0,
+ "DOMEvent had a a latency value generated."
+ );
+ ok(domEventEnd.data.type === "DOMEvent");
+ ok(domEventEnd.name === "DOMEvent");
+ }
+ // Add more marker tests.
+});
diff --git a/tools/profiler/tests/browser/browser_test_profile_capture_by_pid.js b/tools/profiler/tests/browser/browser_test_profile_capture_by_pid.js
new file mode 100644
index 0000000000..3694ffd93e
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_profile_capture_by_pid.js
@@ -0,0 +1,206 @@
+/* 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 ProcessHasSamplerThread(process) {
+ return process.threads.some(t => t.name == "SamplerThread");
+}
+
+async function GetPidsWithSamplerThread() {
+ let parentProc = await ChromeUtils.requestProcInfo();
+
+ let pids = parentProc.children
+ .filter(ProcessHasSamplerThread)
+ .map(proc => proc.pid);
+ if (ProcessHasSamplerThread(parentProc)) {
+ pids.unshift(parentProc.pid);
+ }
+ return pids;
+}
+
+// fnFilterWithContentId: Called with content child pid, returns filters to use.
+// E.g.: 123 => ["GeckoMain", "pid:123"], or 123 => ["pid:456"].
+async function test_with_filter(fnFilterWithContentId) {
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info("Open a tab with single_frame.html in it.");
+ const url = BASE_URL + "single_frame.html";
+ return BrowserTestUtils.withNewTab(url, async function(contentBrowser) {
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ Assert.deepEqual(
+ await GetPidsWithSamplerThread(),
+ [],
+ "There should be no SamplerThreads before starting the profiler"
+ );
+
+ info("Start the profiler to test filters including 'pid:<content>'.");
+ await startProfiler({ threads: fnFilterWithContentId(contentPid) });
+
+ let pidsWithSamplerThread = null;
+ await TestUtils.waitForCondition(
+ async function() {
+ let pidsStringBefore = JSON.stringify(pidsWithSamplerThread);
+ pidsWithSamplerThread = await GetPidsWithSamplerThread();
+ return JSON.stringify(pidsWithSamplerThread) == pidsStringBefore;
+ },
+ "Wait for sampler threads to stabilize after profiler start",
+ /* interval (ms) */ 250,
+ /* maxTries */ 10
+ );
+
+ info("Capture the profile data.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+
+ await TestUtils.waitForCondition(async function() {
+ return !(await GetPidsWithSamplerThread()).length;
+ }, "Wait for all sampler threads to stop after profiler stop");
+
+ return { contentPid, pidsWithSamplerThread, profile };
+ });
+}
+
+add_task(async function browser_test_profile_capture_along_with_content_pid() {
+ const {
+ contentPid,
+ pidsWithSamplerThread,
+ profile,
+ } = await test_with_filter(contentPid => ["GeckoMain", "pid:" + contentPid]);
+
+ Assert.greater(
+ pidsWithSamplerThread.length,
+ 2,
+ "There should be lots of SamplerThreads after starting the profiler"
+ );
+
+ let contentProcessIndex = profile.processes.findIndex(
+ p => p.threads[0].pid == contentPid
+ );
+ Assert.notEqual(
+ contentProcessIndex,
+ -1,
+ "The content process should be present"
+ );
+
+ // Note: Some threads may not be registered, so we can't expect that many. But
+ // 10 is much more than the default 4.
+ Assert.greater(
+ profile.processes[contentProcessIndex].threads.length,
+ 10,
+ "The content process should have many threads"
+ );
+
+ Assert.equal(
+ profile.threads.length,
+ 1,
+ "The parent process should have only one thread"
+ );
+ Assert.equal(
+ profile.threads[0].name,
+ "GeckoMain",
+ "The parent process should have the main thread"
+ );
+});
+
+add_task(async function browser_test_profile_capture_along_with_other_pid() {
+ const parentPid = Services.appinfo.processID;
+ const {
+ contentPid,
+ pidsWithSamplerThread,
+ profile,
+ } = await test_with_filter(contentPid => ["GeckoMain", "pid:" + parentPid]);
+
+ Assert.greater(
+ pidsWithSamplerThread.length,
+ 2,
+ "There should be lots of SamplerThreads after starting the profiler"
+ );
+
+ let contentProcessIndex = profile.processes.findIndex(
+ p => p.threads[0].pid == contentPid
+ );
+ Assert.notEqual(
+ contentProcessIndex,
+ -1,
+ "The content process should be present"
+ );
+
+ Assert.equal(
+ profile.processes[contentProcessIndex].threads.length,
+ 1,
+ "The content process should have only one thread"
+ );
+
+ // Note: Some threads may not be registered, so we can't expect that many. But
+ // 10 is much more than the default 4.
+ Assert.greater(
+ profile.threads.length,
+ 10,
+ "The parent process should have many threads"
+ );
+});
+
+add_task(async function browser_test_profile_capture_by_only_content_pid() {
+ const parentPid = Services.appinfo.processID;
+ const {
+ contentPid,
+ pidsWithSamplerThread,
+ profile,
+ } = await test_with_filter(contentPid => ["pid:" + contentPid]);
+
+ // The sampler thread always runs in the parent process, see bug 1754100.
+ Assert.deepEqual(
+ pidsWithSamplerThread,
+ [parentPid, contentPid],
+ "There should only be SamplerThreads in the parent and the target child"
+ );
+
+ Assert.equal(
+ profile.processes.length,
+ 1,
+ "There should only be one child process"
+ );
+ // Note: Some threads may not be registered, so we can't expect that many. But
+ // 10 is much more than the default 4.
+ Assert.greater(
+ profile.processes[0].threads.length,
+ 10,
+ "The child process should have many threads"
+ );
+ Assert.equal(
+ profile.processes[0].threads[0].pid,
+ contentPid,
+ "The only child process should be our content"
+ );
+});
+
+add_task(async function browser_test_profile_capture_by_only_parent_pid() {
+ const parentPid = Services.appinfo.processID;
+ const {
+ pidsWithSamplerThread,
+ profile,
+ } = await test_with_filter(contentPid => ["pid:" + parentPid]);
+
+ Assert.deepEqual(
+ pidsWithSamplerThread,
+ [parentPid],
+ "There should only be a SamplerThread in the parent"
+ );
+
+ // Note: Some threads may not be registered, so we can't expect that many. But
+ // 10 is much more than the default 4.
+ Assert.greater(
+ profile.threads.length,
+ 10,
+ "The parent process should have many threads"
+ );
+ Assert.equal(
+ profile.processes.length,
+ 0,
+ "There should be no child processes"
+ );
+});
diff --git a/tools/profiler/tests/browser/browser_test_profile_fission.js b/tools/profiler/tests/browser/browser_test_profile_fission.js
new file mode 100644
index 0000000000..f3d8016569
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_profile_fission.js
@@ -0,0 +1,196 @@
+/* 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/. */
+
+if (SpecialPowers.useRemoteSubframes) {
+ // Bug 1586105: these tests could time out in some extremely slow conditions,
+ // when fission is enabled.
+ // Requesting a longer timeout should make it pass.
+ requestLongerTimeout(2);
+}
+
+add_task(async function test_profile_fission_no_private_browsing() {
+ // Requesting the complete log to be able to debug Bug 1586105.
+ SimpleTest.requestCompleteLog();
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still have some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler();
+
+ info("Open a private window with single_frame.html in it.");
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ fission: true,
+ });
+
+ try {
+ const url = BASE_URL_HTTPS + "single_frame.html";
+ const contentBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(contentBrowser, url);
+ await BrowserTestUtils.browserLoaded(contentBrowser, false, url);
+
+ const parentPid = Services.appinfo.processID;
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const activeTabID = contentBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const {
+ profile,
+ contentProcess,
+ contentThread,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+
+ Assert.equal(
+ contentThread.isPrivateBrowsing,
+ false,
+ "The content process has the private browsing flag set to false."
+ );
+
+ Assert.equal(
+ contentThread.userContextId,
+ 0,
+ "The content process has the information about the container used for this process"
+ );
+
+ info(
+ "Check if the captured page is the one with correct values we created."
+ );
+
+ let pageFound = false;
+ for (const page of contentProcess.pages) {
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, false);
+ pageFound = true;
+ break;
+ }
+ }
+ Assert.equal(pageFound, true);
+
+ info("Check that the profiling logs exist with the expected properties.");
+ Assert.equal(typeof profile.profilingLog, "object");
+ Assert.equal(typeof profile.profilingLog[parentPid], "object");
+ const parentLog = profile.profilingLog[parentPid];
+ Assert.equal(typeof parentLog.profilingLogBegin_TSms, "number");
+ Assert.equal(typeof parentLog.profilingLogEnd_TSms, "number");
+ Assert.equal(typeof parentLog.bufferGlobalController, "object");
+ Assert.equal(
+ typeof parentLog.bufferGlobalController.controllerCreationTime_TSms,
+ "number"
+ );
+
+ Assert.equal(typeof profile.profileGatheringLog, "object");
+ Assert.equal(typeof profile.profileGatheringLog[parentPid], "object");
+ Assert.equal(
+ typeof profile.profileGatheringLog[parentPid]
+ .profileGatheringLogBegin_TSms,
+ "number"
+ );
+ Assert.equal(
+ typeof profile.profileGatheringLog[parentPid].profileGatheringLogEnd_TSms,
+ "number"
+ );
+
+ Assert.equal(typeof contentProcess.profilingLog, "object");
+ Assert.equal(typeof contentProcess.profilingLog[contentPid], "object");
+ Assert.equal(
+ typeof contentProcess.profilingLog[contentPid].profilingLogBegin_TSms,
+ "number"
+ );
+ Assert.equal(
+ typeof contentProcess.profilingLog[contentPid].profilingLogEnd_TSms,
+ "number"
+ );
+
+ Assert.equal(typeof contentProcess.profileGatheringLog, "undefined");
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+add_task(async function test_profile_fission_private_browsing() {
+ // Requesting the complete log to be able to debug Bug 1586105.
+ SimpleTest.requestCompleteLog();
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still have some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler();
+
+ info("Open a private window with single_frame.html in it.");
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ fission: true,
+ });
+
+ try {
+ const url = BASE_URL_HTTPS + "single_frame.html";
+ const contentBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(contentBrowser, url);
+ await BrowserTestUtils.browserLoaded(contentBrowser, false, url);
+
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const activeTabID = contentBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const {
+ contentProcess,
+ contentThread,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+
+ Assert.equal(
+ contentThread.isPrivateBrowsing,
+ true,
+ "The content process has the private browsing flag set to true."
+ );
+
+ Assert.equal(
+ contentThread.userContextId,
+ 0,
+ "The content process has the information about the container used for this process"
+ );
+
+ info(
+ "Check if the captured page is the one with correct values we created."
+ );
+
+ let pageFound = false;
+ for (const page of contentProcess.pages) {
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, true);
+ pageFound = true;
+ break;
+ }
+ }
+ Assert.equal(pageFound, true);
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/tools/profiler/tests/browser/browser_test_profile_multi_frame_page_info.js b/tools/profiler/tests/browser/browser_test_profile_multi_frame_page_info.js
new file mode 100644
index 0000000000..6d48306432
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_profile_multi_frame_page_info.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+if (SpecialPowers.useRemoteSubframes) {
+ // Bug 1586105: these tests could time out in some extremely slow conditions,
+ // when fission is enabled.
+ // Requesting a longer timeout should make it pass.
+ requestLongerTimeout(2);
+}
+
+add_task(async function test_profile_multi_frame_page_info() {
+ // Requesting the complete log to be able to debug Bug 1586105.
+ SimpleTest.requestCompleteLog();
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still have some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with multi frame page."
+ );
+ await startProfiler();
+
+ info("Open a tab with multi_frame.html in it.");
+ // multi_frame.html embeds single_frame.html inside an iframe.
+ const url = BASE_URL + "multi_frame.html";
+ await BrowserTestUtils.withNewTab(url, async function(contentBrowser) {
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const activeTabID = win.gBrowser.selectedBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const { contentProcess } = await stopProfilerNowAndGetThreads(contentPid);
+
+ info(
+ "Check if the captured pages are the ones with correct values we created."
+ );
+
+ let parentPage;
+ let foundPage = 0;
+ for (const page of contentProcess.pages) {
+ // Parent page
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, false);
+ parentPage = page;
+ foundPage++;
+ break;
+ }
+ }
+
+ Assert.notEqual(typeof parentPage, "undefined");
+
+ for (const page of contentProcess.pages) {
+ // Child page (iframe)
+ if (page.url == BASE_URL + "single_frame.html") {
+ Assert.equal(page.url, BASE_URL + "single_frame.html");
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ Assert.equal(typeof page.embedderInnerWindowID, "number");
+ Assert.notEqual(typeof parentPage, "undefined");
+ Assert.equal(page.embedderInnerWindowID, parentPage.innerWindowID);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, false);
+ foundPage++;
+ break;
+ }
+ }
+
+ Assert.equal(foundPage, 2);
+ });
+});
diff --git a/tools/profiler/tests/browser/browser_test_profile_single_frame_page_info.js b/tools/profiler/tests/browser/browser_test_profile_single_frame_page_info.js
new file mode 100644
index 0000000000..6f59ae6a7b
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_profile_single_frame_page_info.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+if (SpecialPowers.useRemoteSubframes) {
+ // Bug 1586105: these tests could time out in some extremely slow conditions,
+ // when fission is enabled.
+ // Requesting a longer timeout should make it pass.
+ requestLongerTimeout(2);
+}
+
+add_task(async function test_profile_single_frame_page_info() {
+ // Requesting the complete log to be able to debug Bug 1586105.
+ SimpleTest.requestCompleteLog();
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still have some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler();
+
+ info("Open a tab with single_frame.html in it.");
+ const url = BASE_URL + "single_frame.html";
+ await BrowserTestUtils.withNewTab(url, async function(contentBrowser) {
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const activeTabID = win.gBrowser.selectedBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const { contentProcess } = await stopProfilerNowAndGetThreads(contentPid);
+
+ info(
+ "Check if the captured page is the one with correct values we created."
+ );
+
+ let pageFound = false;
+ for (const page of contentProcess.pages) {
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, false);
+ pageFound = true;
+ break;
+ }
+ }
+ Assert.equal(pageFound, true);
+ });
+});
+
+add_task(async function test_profile_private_browsing() {
+ // Requesting the complete log to be able to debug Bug 1586105.
+ SimpleTest.requestCompleteLog();
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still have some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler();
+
+ info("Open a private window with single_frame.html in it.");
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ fission: false,
+ private: true,
+ });
+
+ try {
+ const url = BASE_URL_HTTPS + "single_frame.html";
+ const contentBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(contentBrowser, url);
+ await BrowserTestUtils.browserLoaded(contentBrowser, false, url);
+
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const activeTabID = contentBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const {
+ contentProcess,
+ contentThread,
+ } = await stopProfilerNowAndGetThreads(contentPid);
+
+ // This information is available with fission only.
+ Assert.equal(
+ contentThread.isPrivateBrowsing,
+ undefined,
+ "The content process has no private browsing flag."
+ );
+
+ Assert.equal(
+ contentThread.userContextId,
+ undefined,
+ "The content process has no information about the container used for this process."
+ );
+
+ info(
+ "Check if the captured page is the one with correct values we created."
+ );
+
+ let pageFound = false;
+ for (const page of contentProcess.pages) {
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ Assert.equal(typeof page.isPrivateBrowsing, "boolean");
+ Assert.equal(page.isPrivateBrowsing, true);
+ pageFound = true;
+ break;
+ }
+ }
+ Assert.equal(pageFound, true);
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/tools/profiler/tests/browser/browser_test_profile_slow_capture.js b/tools/profiler/tests/browser/browser_test_profile_slow_capture.js
new file mode 100644
index 0000000000..83795aaf38
--- /dev/null
+++ b/tools/profiler/tests/browser/browser_test_profile_slow_capture.js
@@ -0,0 +1,104 @@
+/* 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 function browser_test_profile_slow_capture() {
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler({ threads: ["GeckoMain", "test-debug-child-slow-json"] });
+
+ info("Open a tab with single_frame.html in it.");
+ const url = BASE_URL + "single_frame.html";
+ await BrowserTestUtils.withNewTab(url, async function(contentBrowser) {
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ // Getting the active Browser ID to assert the page info tabID later.
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+ const activeTabID = win.gBrowser.selectedBrowser.browsingContext.browserId;
+
+ info("Capture the profile data.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+
+ let pageFound = false;
+ // We need to find the correct content process for that tab.
+ let contentProcess = profile.processes.find(
+ p => p.threads[0].pid == contentPid
+ );
+
+ if (!contentProcess) {
+ throw new Error(
+ `Could not find the content process with given pid: ${contentPid}`
+ );
+ }
+
+ info(
+ "Check if the captured page is the one with correct values we created."
+ );
+
+ for (const page of contentProcess.pages) {
+ if (page.url == url) {
+ Assert.equal(page.url, url);
+ Assert.equal(typeof page.tabID, "number");
+ Assert.equal(page.tabID, activeTabID);
+ Assert.equal(typeof page.innerWindowID, "number");
+ // Top level document will have no embedder.
+ Assert.equal(page.embedderInnerWindowID, 0);
+ pageFound = true;
+ break;
+ }
+ }
+ Assert.equal(pageFound, true);
+
+ info("Flush slow processes with a quick profile.");
+ await startProfiler();
+ for (let i = 0; i < 10; ++i) {
+ await Services.profiler.waitOnePeriodicSampling();
+ }
+ await stopNowAndGetProfile();
+ });
+});
+
+add_task(async function browser_test_profile_very_slow_capture() {
+ Assert.ok(!Services.profiler.IsActive());
+ info("Clear the previous pages just in case we still some open tabs.");
+ await Services.profiler.ClearAllPages();
+
+ info(
+ "Start the profiler to test the page information with single frame page."
+ );
+ await startProfiler({
+ threads: ["GeckoMain", "test-debug-child-very-slow-json"],
+ });
+
+ info("Open a tab with single_frame.html in it.");
+ const url = BASE_URL + "single_frame.html";
+ await BrowserTestUtils.withNewTab(url, async function(contentBrowser) {
+ const contentPid = await SpecialPowers.spawn(contentBrowser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ info("Capture the profile data.");
+ const profile = await waitSamplingAndStopAndGetProfile();
+
+ info("Check that the content process is missing.");
+
+ let contentProcessIndex = profile.processes.findIndex(
+ p => p.threads[0].pid == contentPid
+ );
+ Assert.equal(contentProcessIndex, -1);
+
+ info("Flush slow processes with a quick profile.");
+ await startProfiler();
+ for (let i = 0; i < 10; ++i) {
+ await Services.profiler.waitOnePeriodicSampling();
+ }
+ await stopNowAndGetProfile();
+ });
+});
diff --git a/tools/profiler/tests/browser/do_work_500ms.html b/tools/profiler/tests/browser/do_work_500ms.html
new file mode 100644
index 0000000000..9713a80671
--- /dev/null
+++ b/tools/profiler/tests/browser/do_work_500ms.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Do some work for 500ms</title>
+ <script>
+ const milliseconds = 500;
+ const millisecondsPerBatch = 10;
+ const end = Date.now() + milliseconds;
+ window.total = 0;
+ let i = 0;
+
+ /**
+ * Do work for a set number of milliseconds, but only do the work in batches
+ * so the browser does not get unresponsive.
+ */
+ function doWork() {
+ const batchEnd = Date.now() + millisecondsPerBatch;
+ // Do some work for a set amount of time.
+ while (Date.now() < end) {
+ // Do some kind of work that is non-deterministic to guard against optimizations.
+ window.total += Math.random();
+ i++;
+
+ // Check if a batch is done yet.
+ if (Date.now() > batchEnd) {
+ // Defer the rest of the work into a micro task. Keep on doing this until
+ // the total milliseconds have elapsed.
+ setTimeout(doWork, 0);
+ return;
+ }
+ }
+ }
+
+ doWork();
+ </script>
+</head>
+<body>
+ Do some work for 500ms.
+</body>
+</html>
diff --git a/tools/profiler/tests/browser/firefox-logo-nightly.svg b/tools/profiler/tests/browser/firefox-logo-nightly.svg
new file mode 100644
index 0000000000..f1af370d87
--- /dev/null
+++ b/tools/profiler/tests/browser/firefox-logo-nightly.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 953.37 984"><defs><linearGradient id="linear-gradient" x1="-14706.28" y1="9250.14" x2="-14443.04" y2="9250.14" gradientTransform="matrix(0.76, 0.03, 0.05, -1.12, 11485.47, 11148)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.1" stop-color="#0092f8"/><stop offset="0.31" stop-color="#00abeb"/><stop offset="0.52" stop-color="#00bee1"/><stop offset="0.75" stop-color="#00c8dc"/><stop offset="1" stop-color="#00ccda"/></linearGradient><radialGradient id="radial-gradient" cx="-7588.66" cy="8866.53" r="791.23" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0.02" stop-color="#005fe7"/><stop offset="0.18" stop-color="#0042b4"/><stop offset="0.32" stop-color="#002989"/><stop offset="0.4" stop-color="#002079"/><stop offset="0.47" stop-color="#131d78"/><stop offset="0.66" stop-color="#3b1676"/><stop offset="0.75" stop-color="#4a1475"/></radialGradient><linearGradient id="linear-gradient-2" x1="539.64" y1="254.8" x2="348.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 1, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000f43" stop-opacity="0.4"/><stop offset="0.48" stop-color="#001962" stop-opacity="0.17"/><stop offset="1" stop-color="#002079" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-3" x1="540.64" y1="254.8" x2="349.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" xlink:href="#linear-gradient-2"/><linearGradient id="linear-gradient-4" x1="-8367.12" y1="7348.87" x2="-8482.36" y2="7357.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#812cc9"/><stop offset="1" stop-color="#005fe7"/></linearGradient><linearGradient id="linear-gradient-5" x1="-8449.89" y1="7496.97" x2="-8341.94" y2="7609.09" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.05" stop-color="#005fe7"/><stop offset="0.18" stop-color="#065de6"/><stop offset="0.35" stop-color="#1856e1"/><stop offset="0.56" stop-color="#354adb"/><stop offset="0.78" stop-color="#5d3ad1"/><stop offset="0.95" stop-color="#812cc9"/></linearGradient><linearGradient id="linear-gradient-6" x1="-8653.41" y1="7245.3" x2="-8422.52" y2="7244.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#002079"/><stop offset="0.99" stop-color="#a238ff"/></linearGradient><radialGradient id="radial-gradient-2" cx="644.11" cy="599.83" fx="785.0454815336918" fy="470.6889181532662" r="793.95" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0.2" stop-color="#00fdff"/><stop offset="0.26" stop-color="#0af1ff"/><stop offset="0.37" stop-color="#23d2ff"/><stop offset="0.52" stop-color="#4da0ff"/><stop offset="0.69" stop-color="#855bff"/><stop offset="0.77" stop-color="#a238ff"/><stop offset="0.81" stop-color="#a738fd"/><stop offset="0.86" stop-color="#b539f9"/><stop offset="0.9" stop-color="#cd39f1"/><stop offset="0.96" stop-color="#ee3ae6"/><stop offset="0.98" stop-color="#ff3be0"/></radialGradient><linearGradient id="linear-gradient-7" x1="-7458.97" y1="9093.17" x2="-7531.06" y2="8282.84" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00"/><stop offset="0.1" stop-color="#00e244"/><stop offset="0.22" stop-color="#00d694"/><stop offset="0.31" stop-color="#00cfc7"/><stop offset="0.35" stop-color="#00ccda"/><stop offset="0.42" stop-color="#0bc2dd" stop-opacity="0.92"/><stop offset="0.57" stop-color="#29a7e4" stop-opacity="0.72"/><stop offset="0.77" stop-color="#597df0" stop-opacity="0.4"/><stop offset="1" stop-color="#9448ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-8" x1="-8926.61" y1="7680.53" x2="-8790.14" y2="7680.53" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005fe7"/><stop offset="0.46" stop-color="#0071f3" stop-opacity="0.51"/><stop offset="0.83" stop-color="#007efc" stop-opacity="0.14"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><radialGradient id="radial-gradient-3" cx="-8914.62" cy="7721.05" r="165.97" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.63" stop-color="#ffe302" stop-opacity="0"/><stop offset="0.67" stop-color="#ffe302" stop-opacity="0.05"/><stop offset="0.75" stop-color="#ffe302" stop-opacity="0.19"/><stop offset="0.86" stop-color="#ffe302" stop-opacity="0.4"/><stop offset="0.99" stop-color="#ffe302" stop-opacity="0.7"/></radialGradient><linearGradient id="linear-gradient-9" x1="214.02" y1="2032.47" x2="96.19" y2="2284.31" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, -250.1, 2306.29)" gradientUnits="userSpaceOnUse"><stop offset="0.19" stop-color="#4a1475" stop-opacity="0.5"/><stop offset="0.62" stop-color="#2277ac" stop-opacity="0.23"/><stop offset="0.94" stop-color="#00ccda" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-10" x1="-38.44" y1="278.18" x2="55.67" y2="171.29" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0.01" stop-color="#002079" stop-opacity="0.5"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-11" x1="142.45" y1="96.25" x2="142.5" y2="149.68" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4a1475" stop-opacity="0.9"/><stop offset="0.18" stop-color="#6720a2" stop-opacity="0.6"/><stop offset="0.38" stop-color="#812acb" stop-opacity="0.34"/><stop offset="0.57" stop-color="#9332e8" stop-opacity="0.15"/><stop offset="0.76" stop-color="#9e36f9" stop-opacity="0.04"/><stop offset="0.93" stop-color="#a238ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-12" x1="620.52" y1="947.88" x2="926.18" y2="264.39" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00" stop-opacity="0"/><stop offset="0.28" stop-color="#00dc6d" stop-opacity="0.5"/><stop offset="0.5" stop-color="#00d1bb" stop-opacity="0.86"/><stop offset="0.6" stop-color="#00ccda"/><stop offset="0.68" stop-color="#04c9db"/><stop offset="0.75" stop-color="#0fc1df"/><stop offset="0.83" stop-color="#23b2e6"/><stop offset="0.9" stop-color="#3e9ef0"/><stop offset="0.98" stop-color="#6184fc"/><stop offset="0.99" stop-color="#6680fe"/></linearGradient><linearGradient id="linear-gradient-13" x1="680.88" y1="554.79" x2="536.1" y2="166.04" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.04" stop-color="#0083ff" stop-opacity="0.92"/><stop offset="0.14" stop-color="#0083ff" stop-opacity="0.71"/><stop offset="0.26" stop-color="#0083ff" stop-opacity="0.52"/><stop offset="0.37" stop-color="#0083ff" stop-opacity="0.36"/><stop offset="0.49" stop-color="#0083ff" stop-opacity="0.23"/><stop offset="0.61" stop-color="#0083ff" stop-opacity="0.13"/><stop offset="0.73" stop-color="#0083ff" stop-opacity="0.06"/><stop offset="0.86" stop-color="#0083ff" stop-opacity="0.01"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient></defs><title>firefox-logo-nightly</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Layer_2-2" data-name="Layer 2"><g id="Firefox"><path d="M770.28,91.56c-23.95,27.88-35.1,90.64-10.82,154.26s61.5,49.8,84.7,114.67c30.62,85.6,16.37,200.59,16.37,200.59s36.81,106.61,62.47-6.63C979.79,341.74,770.28,143.94,770.28,91.56Z" style="fill:url(#linear-gradient)"/><path id="_Path_" data-name=" Path " d="M476.92,972.83c245.24,0,443.9-199.74,443.9-446s-198.66-446-443.66-446S33.5,280.51,33.5,526.8C33,773.33,231.92,972.83,476.92,972.83Z" style="fill:url(#radial-gradient)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-2)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-3)"/><path d="M711.1,866.71c162.87-18.86,235-186.7,142.38-190C769.85,674,634,875.61,711.1,866.71Z" style="fill:url(#linear-gradient-4)"/><path d="M865.21,642.42C977.26,577.21,948,436.34,948,436.34s-43.25,50.24-72.62,130.32C846.4,646,797.84,681.81,865.21,642.42Z" style="fill:url(#linear-gradient-5)"/><path d="M509.47,950.06C665.7,999.91,800,876.84,717.21,835.74,642,798.68,435.32,926.49,509.47,950.06Z" style="fill:url(#linear-gradient-6)"/><path d="M638.58,21.42l.53-.57A1.7,1.7,0,0,0,638.58,21.42ZM876.85,702.23c3.8-5.36,8.94-22.53,13.48-30.21,27.58-44.52,27.78-80,27.78-80.84,16.66-83.22,15.15-117.2,4.9-180-8.25-50.6-44.32-123.09-75.57-158-32.2-36-9.51-24.25-40.69-50.52-27.33-30.29-53.82-60.29-68.25-72.36C634.22,43.09,636.57,24.58,638.58,21.42c-.34.37-.84.92-1.47,1.64C635.87,18.14,635,14,635,14s-57,57-69,152c-7.83,62,15.38,126.68,49,168a381.62,381.62,0,0,0,59,58h0c25.4,36.48,39.38,81.49,39.38,129.91,0,121.24-98.34,219.53-219.65,219.53a220.14,220.14,0,0,1-49.13-5.52c-57.24-10.92-90.3-39.8-106.78-59.41-9.45-11.23-13.46-19.42-13.46-19.42,51.28,18.37,108,14.53,142.47-4.52,34.75-19.26,55.77-33.55,72.84-27.92,16.82,5.61,30.21-10.67,18.2-27.54-11.77-16.85-42.4-41-87.88-34.29-34.79,5.07-66.66,29.76-112.24,5.84a97.34,97.34,0,0,1-8.55-5c-3-1.77,9.77,2.69,6.79.68-8.87-4.32-24.57-13.73-28.64-17.07-.68-.56,6.88,2.16,6.2,1.6-42.62-31.45-37.3-52.69-36-66,1.07-10.66,8.81-24.32,21.86-29.86,6.3,3.08,10.23,5.43,10.23,5.43s-2.69-4.92-4.14-7.51c.51-.19,1-.15,1.5-.34,5.16,2.23,16.58,8,22.59,11.57,7.83,4.95,10.32,9.36,10.32,9.36s2.06-1,.54-5.33c-.56-1.77-2.93-7.39-10.68-13.07h.48a91.65,91.65,0,0,1,13.13,8.17c2.19-7.12,6.12-14.56,5.25-27.86-.53-9.35-.28-11.78-2.12-15.39-1.65-3.1.92-4.31,3.78-1.09a29.73,29.73,0,0,0-2.44-7.34v-.24c3.57-11.14,75.53-40.12,80.77-43.51a70.24,70.24,0,0,0,21.17-20.63c4-5.72,7-13.73,7.75-25.89.25-5.48-1.44-9.82-20.5-14-11.44-2.49-29.14-4.91-56.43-7.47-19.9-1.76-31.58-14.68-38.21-26.6-1.21-2.57-2.45-4.9-3.68-7.22a53.41,53.41,0,0,1-2.83-8.36,158.47,158.47,0,0,1,61.28-76.06c1.6-1.31-6.4.33-4.8-1,1.87-1.52,14.06-5.93,16.37-6.92,2.81-1.19-12-6.84-25.16-5.47-13.36,1.35-16.19,2.78-23.32,5.49,3-2.64,12.37-6.1,10.16-6.08-14.4,2-32.3,9.48-47.6,18a9.72,9.72,0,0,1,.92-4.31c-7.13,2.71-24.64,13.67-29.73,23a39.79,39.79,0,0,0,.29-5.35,88.55,88.55,0,0,0-14.6,13.7l-.27.22C258.14,196,221.75,195,191,201.72c-6.74-6.06-17.57-15.23-32.89-45.4-1-1.82-1.6,3.75-2.4,2-6-13.81-9.55-36.44-9-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.87-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.37,12.62-1.38,4-3.32,6.27-4.56,11.29l-.29.46c-.1-1.48.37-6.08,0-5.14A235.4,235.4,0,0,0,95.34,186c-5.49,18-11.88,42.61-12.89,74.57-.24,2.42,0,5.14-.25,7.32-13,14.83-21.86,27.39-25.2,33.91-16.81,26-35.33,66.44-53.29,130.46a319.35,319.35,0,0,1,28.54-50C17.32,416.25,2.89,469.62,0,551.8a436.92,436.92,0,0,1,13.87-50.24C11.29,556.36,17.68,624.3,52.32,701c20.57,45,67.92,136.6,183.62,208h0s39.36,29.3,107,51.26c5,1.81,10.06,3.6,15.23,5.33q-2.43-1-4.71-2A484.9,484.9,0,0,0,492.27,984c175.18.15,226.85-70.2,226.85-70.2l-.51.38q3.71-3.49,7.14-7.26c-27.64,26.08-90.75,27.84-114.3,26,40.22-11.81,66.69-21.81,118.17-41.52q9-3.36,18.48-7.64l2-.94c1.25-.58,2.49-1.13,3.75-1.74a349.3,349.3,0,0,0,70.26-44c51.7-41.3,63-81.56,68.83-108.1-.82,2.54-3.37,8.47-5.17,12.32-13.31,28.48-42.84,46-74.91,61a689.05,689.05,0,0,0,42.38-62.44C865.77,729.39,869,713.15,876.85,702.23Z" style="fill:url(#radial-gradient-2)"/><path d="M813.92,801c21.08-23.24,40-49.82,54.35-80,36.9-77.58,94-206.58,49-341.31C881.77,273.22,833,215,771.11,158.12,670.56,65.76,642.48,24.52,642.48,0c0,0-116.09,129.41-65.74,264.38s153.46,130,221.68,270.87c80.27,165.74-64.95,346.61-185,397.24,7.35-1.63,267-60.38,280.61-208.88C893.68,726.34,887.83,767.41,813.92,801Z" style="fill:url(#linear-gradient-7)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="fill:url(#linear-gradient-8)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="opacity:0.5;isolation:isolate;fill:url(#radial-gradient-3)"/><path d="M158.31,156.47c-1-1.82-1.6,3.75-2.4,2-6-13.81-9.58-36.2-8.72-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.86-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.35,12.38-1.65,4.24-3.35,6.52-4.61,11.77-.39,1.43.39-6.32,0-5.38C84.72,201.68,80.19,271,82.69,268,133.17,214.14,191,201.36,191,201.36c-6.15-4.53-19.53-17.63-32.7-44.89Z" style="fill:url(#linear-gradient-9)"/><path d="M349.84,720.1c-69.72-29.77-149-71.75-146-167.14C207.92,427.35,321,452.18,321,452.18c-4.27,1-15.68,9.16-19.72,17.82-4.27,10.83-12.07,35.28,11.55,60.9,37.09,40.19-76.2,95.36,98.66,199.57,4.41,2.4-41-1.43-61.64-10.36Z" style="fill:url(#linear-gradient-10)"/><path d="M325.07,657.5c49.44,17.21,107,14.19,141.52-4.86,23.09-12.85,52.7-33.43,70.92-28.35-15.78-6.24-27.73-9.15-42.1-9.86-2.45,0-5.38,0-8-.32a136,136,0,0,0-15.76.86c-8.9.82-18.77,6.43-27.74,5.53-.48,0,8.7-3.77,8-3.61-4.75,1-9.92,1.21-15.37,1.88-3.47.39-6.45.82-9.89,1-103,8.73-190-55.81-190-55.81-7.41,25,33.17,74.3,88.52,93.57Z" style="opacity:0.5;isolation:isolate;fill:url(#linear-gradient-11)"/><path d="M813.74,801.65c104.16-102.27,156.86-226.58,134.58-366,0,0,8.9,71.5-24.85,144.63,16.21-71.39,18.1-160.11-25-252C841,205.64,746.45,141.11,710.35,114.19,655.66,73.4,633,31.87,632.57,23.3c-16.34,33.48-65.77,148.2-5.31,247,56.64,92.56,145.86,120,208.33,205C950.67,631.67,813.74,801.65,813.74,801.65Z" style="fill:url(#linear-gradient-12)"/><path d="M798.81,535.55C762.41,460.35,717,427.55,674,392c5,7,6.23,9.47,9,14,37.83,40.32,93.61,138.66,53.11,262.11C659.88,900.48,355,791.06,323,760.32,335.93,894.81,561,959.16,707.6,872,791,793,858.47,658.79,798.81,535.55Z" style="fill:url(#linear-gradient-13)"/></g></g></g></g></svg> \ No newline at end of file
diff --git a/tools/profiler/tests/browser/head.js b/tools/profiler/tests/browser/head.js
new file mode 100644
index 0000000000..ef0e3128c0
--- /dev/null
+++ b/tools/profiler/tests/browser/head.js
@@ -0,0 +1,159 @@
+/* import-globals-from ../shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/tools/profiler/tests/browser/shared-head.js",
+ this
+);
+
+const BASE_URL = "http://example.com/browser/tools/profiler/tests/browser/";
+const BASE_URL_HTTPS =
+ "https://example.com/browser/tools/profiler/tests/browser/";
+
+registerCleanupFunction(async () => {
+ if (Services.profiler.IsActive()) {
+ info(
+ "The profiler was found to still be running at the end of the test, which means that some error likely occured. Let's stop it to prevent issues with following tests!"
+ );
+ await Services.profiler.StopProfiler();
+ }
+});
+
+/**
+ * This is a helper function that will stop the profiler and returns the main
+ * threads for the parent process and the content process with PID contentPid.
+ * This happens immediately, without waiting for any sampling to happen or
+ * finish. Use waitSamplingAndStopProfilerAndGetThreads below instead to wait
+ * for samples before stopping.
+ * This returns also the full profile in case the caller wants more information.
+ *
+ * @param {number} contentPid
+ * @returns {Promise<{profile, parentThread, contentProcess, contentThread}>}
+ */
+async function stopProfilerNowAndGetThreads(contentPid) {
+ const profile = await stopNowAndGetProfile();
+
+ const parentThread = profile.threads[0];
+ const contentProcess = profile.processes.find(
+ p => p.threads[0].pid == contentPid
+ );
+ if (!contentProcess) {
+ throw new Error(
+ `Could not find the content process with given pid: ${contentPid}`
+ );
+ }
+
+ if (!parentThread) {
+ throw new Error("The parent thread was not found in the profile.");
+ }
+
+ const contentThread = contentProcess.threads[0];
+ if (!contentThread) {
+ throw new Error("The content thread was not found in the profile.");
+ }
+
+ return { profile, parentThread, contentProcess, contentThread };
+}
+
+/**
+ * This is a helper function that will stop the profiler and returns the main
+ * threads for the parent process and the content process with PID contentPid.
+ * As opposed to stopProfilerNowAndGetThreads (with "Now") above, the profiler
+ * in that PID will not stop until there is at least one periodic sample taken.
+ *
+ * @param {number} contentPid
+ * @returns {Promise<{profile, parentThread, contentProcess, contentThread}>}
+ */
+async function waitSamplingAndStopProfilerAndGetThreads(contentPid) {
+ await Services.profiler.waitOnePeriodicSampling();
+
+ return stopProfilerNowAndGetThreads(contentPid);
+}
+
+/** This tries to find the service worker thread by targeting a very specific
+ * UserTiming marker. Indeed we use performance.mark to add this marker from the
+ * service worker's events.
+ * Then from this thread we get its parent thread. Indeed the parent thread is
+ * where all network stuff happens, so this is useful for network marker tests.
+ *
+ * @param {Object} profile
+ * @returns {{ serviceWorkerThread: Object, serviceWorkerParentThread: Object }} the found threads
+ */
+function findServiceWorkerThreads(profile) {
+ const allThreads = [
+ profile.threads,
+ ...profile.processes.map(process => process.threads),
+ ].flat();
+
+ const serviceWorkerThread = allThreads.find(
+ ({ processType, markers }) =>
+ processType === "tab" &&
+ markers.data.some(markerTuple => {
+ const data = markerTuple[markers.schema.data];
+ return (
+ data &&
+ data.type === "UserTiming" &&
+ data.name === "__serviceworker_event"
+ );
+ })
+ );
+
+ if (!serviceWorkerThread) {
+ info(
+ "We couldn't find a service worker thread. Here are all the threads in this profile:"
+ );
+ allThreads.forEach(logInformationForThread.bind(null, ""));
+ return null;
+ }
+
+ const serviceWorkerParentThread = allThreads.find(
+ ({ name, pid }) => pid === serviceWorkerThread.pid && name === "GeckoMain"
+ );
+
+ if (!serviceWorkerParentThread) {
+ info(
+ `We couldn't find a parent thread for the service worker thread (pid: ${serviceWorkerThread.pid}, tid: ${serviceWorkerThread.tid}).`
+ );
+ info("Here are all the threads in this profile:");
+ allThreads.forEach(logInformationForThread.bind(null, ""));
+
+ // Let's write the profile on disk if MOZ_UPLOAD_DIR is present
+ const path = Services.env.get("MOZ_UPLOAD_DIR");
+ if (path) {
+ const profileName = `profile_${Date.now()}.json`;
+ const profilePath = PathUtils.join(path, profileName);
+ info(
+ `We wrote down the profile on disk as an artifact, with name ${profileName}.`
+ );
+ // This function returns a Promise, but we're not waiting on it because
+ // we're in a synchronous function. Hopefully writing will be finished
+ // when the process ends.
+ IOUtils.writeJSON(profilePath, profile).catch(err =>
+ console.error("An error happened when writing the profile on disk", err)
+ );
+ }
+ throw new Error(
+ "We couldn't find a parent thread for the service worker thread. Please read logs to find more information."
+ );
+ }
+
+ return { serviceWorkerThread, serviceWorkerParentThread };
+}
+
+/**
+ * This logs some basic information about the passed thread.
+ *
+ * @param {string} prefix
+ * @param {Object} thread
+ */
+function logInformationForThread(prefix, thread) {
+ if (!thread) {
+ info(prefix + ": thread is null or undefined.");
+ return;
+ }
+
+ const { name, pid, tid, processName, processType } = thread;
+ info(
+ `${prefix}: ` +
+ `name(${name}) pid(${pid}) tid(${tid}) processName(${processName}) processType(${processType})`
+ );
+}
diff --git a/tools/profiler/tests/browser/multi_frame.html b/tools/profiler/tests/browser/multi_frame.html
new file mode 100644
index 0000000000..b2efcedd50
--- /dev/null
+++ b/tools/profiler/tests/browser/multi_frame.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Multi Frame</title>
+</head>
+<body>
+ Multi Frame
+ <iframe src="single_frame.html"></iframe>
+</body>
+</html>
diff --git a/tools/profiler/tests/browser/page_with_resources.html b/tools/profiler/tests/browser/page_with_resources.html
new file mode 100644
index 0000000000..9d2bb8f218
--- /dev/null
+++ b/tools/profiler/tests/browser/page_with_resources.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ Testing
+ <img src='firefox-logo-nightly.svg' width="24"/>
+ <img src='redirect.sjs?firefox-logo-nightly.svg' width="24"/>
+ </body>
+</html>
diff --git a/tools/profiler/tests/browser/redirect.sjs b/tools/profiler/tests/browser/redirect.sjs
new file mode 100644
index 0000000000..2a325c3d0b
--- /dev/null
+++ b/tools/profiler/tests/browser/redirect.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader(
+ "Location",
+ decodeURIComponent(request.queryString),
+ false
+ );
+}
diff --git a/tools/profiler/tests/browser/serviceworkers/firefox-logo-nightly.svg b/tools/profiler/tests/browser/serviceworkers/firefox-logo-nightly.svg
new file mode 100644
index 0000000000..f1af370d87
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/firefox-logo-nightly.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 953.37 984"><defs><linearGradient id="linear-gradient" x1="-14706.28" y1="9250.14" x2="-14443.04" y2="9250.14" gradientTransform="matrix(0.76, 0.03, 0.05, -1.12, 11485.47, 11148)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.1" stop-color="#0092f8"/><stop offset="0.31" stop-color="#00abeb"/><stop offset="0.52" stop-color="#00bee1"/><stop offset="0.75" stop-color="#00c8dc"/><stop offset="1" stop-color="#00ccda"/></linearGradient><radialGradient id="radial-gradient" cx="-7588.66" cy="8866.53" r="791.23" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0.02" stop-color="#005fe7"/><stop offset="0.18" stop-color="#0042b4"/><stop offset="0.32" stop-color="#002989"/><stop offset="0.4" stop-color="#002079"/><stop offset="0.47" stop-color="#131d78"/><stop offset="0.66" stop-color="#3b1676"/><stop offset="0.75" stop-color="#4a1475"/></radialGradient><linearGradient id="linear-gradient-2" x1="539.64" y1="254.8" x2="348.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 1, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000f43" stop-opacity="0.4"/><stop offset="0.48" stop-color="#001962" stop-opacity="0.17"/><stop offset="1" stop-color="#002079" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-3" x1="540.64" y1="254.8" x2="349.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" xlink:href="#linear-gradient-2"/><linearGradient id="linear-gradient-4" x1="-8367.12" y1="7348.87" x2="-8482.36" y2="7357.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#812cc9"/><stop offset="1" stop-color="#005fe7"/></linearGradient><linearGradient id="linear-gradient-5" x1="-8449.89" y1="7496.97" x2="-8341.94" y2="7609.09" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.05" stop-color="#005fe7"/><stop offset="0.18" stop-color="#065de6"/><stop offset="0.35" stop-color="#1856e1"/><stop offset="0.56" stop-color="#354adb"/><stop offset="0.78" stop-color="#5d3ad1"/><stop offset="0.95" stop-color="#812cc9"/></linearGradient><linearGradient id="linear-gradient-6" x1="-8653.41" y1="7245.3" x2="-8422.52" y2="7244.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#002079"/><stop offset="0.99" stop-color="#a238ff"/></linearGradient><radialGradient id="radial-gradient-2" cx="644.11" cy="599.83" fx="785.0454815336918" fy="470.6889181532662" r="793.95" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0.2" stop-color="#00fdff"/><stop offset="0.26" stop-color="#0af1ff"/><stop offset="0.37" stop-color="#23d2ff"/><stop offset="0.52" stop-color="#4da0ff"/><stop offset="0.69" stop-color="#855bff"/><stop offset="0.77" stop-color="#a238ff"/><stop offset="0.81" stop-color="#a738fd"/><stop offset="0.86" stop-color="#b539f9"/><stop offset="0.9" stop-color="#cd39f1"/><stop offset="0.96" stop-color="#ee3ae6"/><stop offset="0.98" stop-color="#ff3be0"/></radialGradient><linearGradient id="linear-gradient-7" x1="-7458.97" y1="9093.17" x2="-7531.06" y2="8282.84" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00"/><stop offset="0.1" stop-color="#00e244"/><stop offset="0.22" stop-color="#00d694"/><stop offset="0.31" stop-color="#00cfc7"/><stop offset="0.35" stop-color="#00ccda"/><stop offset="0.42" stop-color="#0bc2dd" stop-opacity="0.92"/><stop offset="0.57" stop-color="#29a7e4" stop-opacity="0.72"/><stop offset="0.77" stop-color="#597df0" stop-opacity="0.4"/><stop offset="1" stop-color="#9448ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-8" x1="-8926.61" y1="7680.53" x2="-8790.14" y2="7680.53" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005fe7"/><stop offset="0.46" stop-color="#0071f3" stop-opacity="0.51"/><stop offset="0.83" stop-color="#007efc" stop-opacity="0.14"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><radialGradient id="radial-gradient-3" cx="-8914.62" cy="7721.05" r="165.97" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.63" stop-color="#ffe302" stop-opacity="0"/><stop offset="0.67" stop-color="#ffe302" stop-opacity="0.05"/><stop offset="0.75" stop-color="#ffe302" stop-opacity="0.19"/><stop offset="0.86" stop-color="#ffe302" stop-opacity="0.4"/><stop offset="0.99" stop-color="#ffe302" stop-opacity="0.7"/></radialGradient><linearGradient id="linear-gradient-9" x1="214.02" y1="2032.47" x2="96.19" y2="2284.31" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, -250.1, 2306.29)" gradientUnits="userSpaceOnUse"><stop offset="0.19" stop-color="#4a1475" stop-opacity="0.5"/><stop offset="0.62" stop-color="#2277ac" stop-opacity="0.23"/><stop offset="0.94" stop-color="#00ccda" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-10" x1="-38.44" y1="278.18" x2="55.67" y2="171.29" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0.01" stop-color="#002079" stop-opacity="0.5"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-11" x1="142.45" y1="96.25" x2="142.5" y2="149.68" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4a1475" stop-opacity="0.9"/><stop offset="0.18" stop-color="#6720a2" stop-opacity="0.6"/><stop offset="0.38" stop-color="#812acb" stop-opacity="0.34"/><stop offset="0.57" stop-color="#9332e8" stop-opacity="0.15"/><stop offset="0.76" stop-color="#9e36f9" stop-opacity="0.04"/><stop offset="0.93" stop-color="#a238ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-12" x1="620.52" y1="947.88" x2="926.18" y2="264.39" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00" stop-opacity="0"/><stop offset="0.28" stop-color="#00dc6d" stop-opacity="0.5"/><stop offset="0.5" stop-color="#00d1bb" stop-opacity="0.86"/><stop offset="0.6" stop-color="#00ccda"/><stop offset="0.68" stop-color="#04c9db"/><stop offset="0.75" stop-color="#0fc1df"/><stop offset="0.83" stop-color="#23b2e6"/><stop offset="0.9" stop-color="#3e9ef0"/><stop offset="0.98" stop-color="#6184fc"/><stop offset="0.99" stop-color="#6680fe"/></linearGradient><linearGradient id="linear-gradient-13" x1="680.88" y1="554.79" x2="536.1" y2="166.04" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.04" stop-color="#0083ff" stop-opacity="0.92"/><stop offset="0.14" stop-color="#0083ff" stop-opacity="0.71"/><stop offset="0.26" stop-color="#0083ff" stop-opacity="0.52"/><stop offset="0.37" stop-color="#0083ff" stop-opacity="0.36"/><stop offset="0.49" stop-color="#0083ff" stop-opacity="0.23"/><stop offset="0.61" stop-color="#0083ff" stop-opacity="0.13"/><stop offset="0.73" stop-color="#0083ff" stop-opacity="0.06"/><stop offset="0.86" stop-color="#0083ff" stop-opacity="0.01"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient></defs><title>firefox-logo-nightly</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Layer_2-2" data-name="Layer 2"><g id="Firefox"><path d="M770.28,91.56c-23.95,27.88-35.1,90.64-10.82,154.26s61.5,49.8,84.7,114.67c30.62,85.6,16.37,200.59,16.37,200.59s36.81,106.61,62.47-6.63C979.79,341.74,770.28,143.94,770.28,91.56Z" style="fill:url(#linear-gradient)"/><path id="_Path_" data-name=" Path " d="M476.92,972.83c245.24,0,443.9-199.74,443.9-446s-198.66-446-443.66-446S33.5,280.51,33.5,526.8C33,773.33,231.92,972.83,476.92,972.83Z" style="fill:url(#radial-gradient)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-2)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-3)"/><path d="M711.1,866.71c162.87-18.86,235-186.7,142.38-190C769.85,674,634,875.61,711.1,866.71Z" style="fill:url(#linear-gradient-4)"/><path d="M865.21,642.42C977.26,577.21,948,436.34,948,436.34s-43.25,50.24-72.62,130.32C846.4,646,797.84,681.81,865.21,642.42Z" style="fill:url(#linear-gradient-5)"/><path d="M509.47,950.06C665.7,999.91,800,876.84,717.21,835.74,642,798.68,435.32,926.49,509.47,950.06Z" style="fill:url(#linear-gradient-6)"/><path d="M638.58,21.42l.53-.57A1.7,1.7,0,0,0,638.58,21.42ZM876.85,702.23c3.8-5.36,8.94-22.53,13.48-30.21,27.58-44.52,27.78-80,27.78-80.84,16.66-83.22,15.15-117.2,4.9-180-8.25-50.6-44.32-123.09-75.57-158-32.2-36-9.51-24.25-40.69-50.52-27.33-30.29-53.82-60.29-68.25-72.36C634.22,43.09,636.57,24.58,638.58,21.42c-.34.37-.84.92-1.47,1.64C635.87,18.14,635,14,635,14s-57,57-69,152c-7.83,62,15.38,126.68,49,168a381.62,381.62,0,0,0,59,58h0c25.4,36.48,39.38,81.49,39.38,129.91,0,121.24-98.34,219.53-219.65,219.53a220.14,220.14,0,0,1-49.13-5.52c-57.24-10.92-90.3-39.8-106.78-59.41-9.45-11.23-13.46-19.42-13.46-19.42,51.28,18.37,108,14.53,142.47-4.52,34.75-19.26,55.77-33.55,72.84-27.92,16.82,5.61,30.21-10.67,18.2-27.54-11.77-16.85-42.4-41-87.88-34.29-34.79,5.07-66.66,29.76-112.24,5.84a97.34,97.34,0,0,1-8.55-5c-3-1.77,9.77,2.69,6.79.68-8.87-4.32-24.57-13.73-28.64-17.07-.68-.56,6.88,2.16,6.2,1.6-42.62-31.45-37.3-52.69-36-66,1.07-10.66,8.81-24.32,21.86-29.86,6.3,3.08,10.23,5.43,10.23,5.43s-2.69-4.92-4.14-7.51c.51-.19,1-.15,1.5-.34,5.16,2.23,16.58,8,22.59,11.57,7.83,4.95,10.32,9.36,10.32,9.36s2.06-1,.54-5.33c-.56-1.77-2.93-7.39-10.68-13.07h.48a91.65,91.65,0,0,1,13.13,8.17c2.19-7.12,6.12-14.56,5.25-27.86-.53-9.35-.28-11.78-2.12-15.39-1.65-3.1.92-4.31,3.78-1.09a29.73,29.73,0,0,0-2.44-7.34v-.24c3.57-11.14,75.53-40.12,80.77-43.51a70.24,70.24,0,0,0,21.17-20.63c4-5.72,7-13.73,7.75-25.89.25-5.48-1.44-9.82-20.5-14-11.44-2.49-29.14-4.91-56.43-7.47-19.9-1.76-31.58-14.68-38.21-26.6-1.21-2.57-2.45-4.9-3.68-7.22a53.41,53.41,0,0,1-2.83-8.36,158.47,158.47,0,0,1,61.28-76.06c1.6-1.31-6.4.33-4.8-1,1.87-1.52,14.06-5.93,16.37-6.92,2.81-1.19-12-6.84-25.16-5.47-13.36,1.35-16.19,2.78-23.32,5.49,3-2.64,12.37-6.1,10.16-6.08-14.4,2-32.3,9.48-47.6,18a9.72,9.72,0,0,1,.92-4.31c-7.13,2.71-24.64,13.67-29.73,23a39.79,39.79,0,0,0,.29-5.35,88.55,88.55,0,0,0-14.6,13.7l-.27.22C258.14,196,221.75,195,191,201.72c-6.74-6.06-17.57-15.23-32.89-45.4-1-1.82-1.6,3.75-2.4,2-6-13.81-9.55-36.44-9-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.87-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.37,12.62-1.38,4-3.32,6.27-4.56,11.29l-.29.46c-.1-1.48.37-6.08,0-5.14A235.4,235.4,0,0,0,95.34,186c-5.49,18-11.88,42.61-12.89,74.57-.24,2.42,0,5.14-.25,7.32-13,14.83-21.86,27.39-25.2,33.91-16.81,26-35.33,66.44-53.29,130.46a319.35,319.35,0,0,1,28.54-50C17.32,416.25,2.89,469.62,0,551.8a436.92,436.92,0,0,1,13.87-50.24C11.29,556.36,17.68,624.3,52.32,701c20.57,45,67.92,136.6,183.62,208h0s39.36,29.3,107,51.26c5,1.81,10.06,3.6,15.23,5.33q-2.43-1-4.71-2A484.9,484.9,0,0,0,492.27,984c175.18.15,226.85-70.2,226.85-70.2l-.51.38q3.71-3.49,7.14-7.26c-27.64,26.08-90.75,27.84-114.3,26,40.22-11.81,66.69-21.81,118.17-41.52q9-3.36,18.48-7.64l2-.94c1.25-.58,2.49-1.13,3.75-1.74a349.3,349.3,0,0,0,70.26-44c51.7-41.3,63-81.56,68.83-108.1-.82,2.54-3.37,8.47-5.17,12.32-13.31,28.48-42.84,46-74.91,61a689.05,689.05,0,0,0,42.38-62.44C865.77,729.39,869,713.15,876.85,702.23Z" style="fill:url(#radial-gradient-2)"/><path d="M813.92,801c21.08-23.24,40-49.82,54.35-80,36.9-77.58,94-206.58,49-341.31C881.77,273.22,833,215,771.11,158.12,670.56,65.76,642.48,24.52,642.48,0c0,0-116.09,129.41-65.74,264.38s153.46,130,221.68,270.87c80.27,165.74-64.95,346.61-185,397.24,7.35-1.63,267-60.38,280.61-208.88C893.68,726.34,887.83,767.41,813.92,801Z" style="fill:url(#linear-gradient-7)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="fill:url(#linear-gradient-8)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="opacity:0.5;isolation:isolate;fill:url(#radial-gradient-3)"/><path d="M158.31,156.47c-1-1.82-1.6,3.75-2.4,2-6-13.81-9.58-36.2-8.72-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.86-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.35,12.38-1.65,4.24-3.35,6.52-4.61,11.77-.39,1.43.39-6.32,0-5.38C84.72,201.68,80.19,271,82.69,268,133.17,214.14,191,201.36,191,201.36c-6.15-4.53-19.53-17.63-32.7-44.89Z" style="fill:url(#linear-gradient-9)"/><path d="M349.84,720.1c-69.72-29.77-149-71.75-146-167.14C207.92,427.35,321,452.18,321,452.18c-4.27,1-15.68,9.16-19.72,17.82-4.27,10.83-12.07,35.28,11.55,60.9,37.09,40.19-76.2,95.36,98.66,199.57,4.41,2.4-41-1.43-61.64-10.36Z" style="fill:url(#linear-gradient-10)"/><path d="M325.07,657.5c49.44,17.21,107,14.19,141.52-4.86,23.09-12.85,52.7-33.43,70.92-28.35-15.78-6.24-27.73-9.15-42.1-9.86-2.45,0-5.38,0-8-.32a136,136,0,0,0-15.76.86c-8.9.82-18.77,6.43-27.74,5.53-.48,0,8.7-3.77,8-3.61-4.75,1-9.92,1.21-15.37,1.88-3.47.39-6.45.82-9.89,1-103,8.73-190-55.81-190-55.81-7.41,25,33.17,74.3,88.52,93.57Z" style="opacity:0.5;isolation:isolate;fill:url(#linear-gradient-11)"/><path d="M813.74,801.65c104.16-102.27,156.86-226.58,134.58-366,0,0,8.9,71.5-24.85,144.63,16.21-71.39,18.1-160.11-25-252C841,205.64,746.45,141.11,710.35,114.19,655.66,73.4,633,31.87,632.57,23.3c-16.34,33.48-65.77,148.2-5.31,247,56.64,92.56,145.86,120,208.33,205C950.67,631.67,813.74,801.65,813.74,801.65Z" style="fill:url(#linear-gradient-12)"/><path d="M798.81,535.55C762.41,460.35,717,427.55,674,392c5,7,6.23,9.47,9,14,37.83,40.32,93.61,138.66,53.11,262.11C659.88,900.48,355,791.06,323,760.32,335.93,894.81,561,959.16,707.6,872,791,793,858.47,658.79,798.81,535.55Z" style="fill:url(#linear-gradient-13)"/></g></g></g></g></svg> \ No newline at end of file
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker-utils.js b/tools/profiler/tests/browser/serviceworkers/serviceworker-utils.js
new file mode 100644
index 0000000000..16a9f0c91f
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker-utils.js
@@ -0,0 +1,39 @@
+// Most of this file has been stolen from dom/serviceworkers/test/utils.js.
+
+function waitForState(worker, state) {
+ return new Promise((resolve, reject) => {
+ function onStateChange() {
+ if (worker.state === state) {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ if (worker.state === "redundant") {
+ worker.removeEventListener("statechange", onStateChange);
+ reject(new Error("The service worker failed to install."));
+ }
+ }
+
+ // First add an event listener, so we won't miss any change that happens
+ // before we check the current state.
+ worker.addEventListener("statechange", onStateChange);
+
+ // Now check if the worker is already in the desired state.
+ onStateChange();
+ });
+}
+
+async function registerServiceWorkerAndWait(serviceWorkerFile) {
+ if (!serviceWorkerFile) {
+ throw new Error(
+ "No service worker filename has been specified. Please specify a valid filename."
+ );
+ }
+
+ console.log(`...registering the serviceworker "${serviceWorkerFile}"`);
+ const reg = await navigator.serviceWorker.register(`./${serviceWorkerFile}`, {
+ scope: "./",
+ });
+ console.log("...waiting for activation");
+ await waitForState(reg.installing, "activated");
+ console.log("...activated!");
+}
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_cache_first.js b/tools/profiler/tests/browser/serviceworkers/serviceworker_cache_first.js
new file mode 100644
index 0000000000..baa07fd6d8
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_cache_first.js
@@ -0,0 +1,34 @@
+const files = ["serviceworker_page.html", "firefox-logo-nightly.svg"];
+const cacheName = "v1";
+
+self.addEventListener("install", event => {
+ performance.mark("__serviceworker_event");
+ console.log("[SW]:", "Install event");
+
+ event.waitUntil(cacheAssets());
+});
+
+async function cacheAssets() {
+ const cache = await caches.open(cacheName);
+ await cache.addAll(files);
+}
+
+self.addEventListener("fetch", event => {
+ performance.mark("__serviceworker_event");
+ console.log("Handling fetch event for", event.request.url);
+ event.respondWith(handleFetch(event.request));
+});
+
+async function handleFetch(request) {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ console.log("Found response in cache:", cachedResponse);
+
+ return cachedResponse;
+ }
+ console.log("No response found in cache. About to fetch from network...");
+
+ const networkResponse = await fetch(request);
+ console.log("Response from network is:", networkResponse);
+ return networkResponse;
+}
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_no_fetch_handler.js b/tools/profiler/tests/browser/serviceworkers/serviceworker_no_fetch_handler.js
new file mode 100644
index 0000000000..f656665ca0
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_no_fetch_handler.js
@@ -0,0 +1,4 @@
+self.addEventListener("install", event => {
+ performance.mark("__serviceworker_event");
+ console.log("[SW]:", "Install event");
+});
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_no_respondWith_in_fetch_handler.js b/tools/profiler/tests/browser/serviceworkers/serviceworker_no_respondWith_in_fetch_handler.js
new file mode 100644
index 0000000000..255c8269a1
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_no_respondWith_in_fetch_handler.js
@@ -0,0 +1,9 @@
+self.addEventListener("install", event => {
+ performance.mark("__serviceworker_event");
+ console.log("[SW]:", "Install event");
+});
+
+self.addEventListener("fetch", event => {
+ performance.mark("__serviceworker_event");
+ console.log("Handling fetch event for", event.request.url);
+});
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_page.html b/tools/profiler/tests/browser/serviceworkers/serviceworker_page.html
new file mode 100644
index 0000000000..1c2100a9d6
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_page.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <meta name='viewport' content='initial-scale=1'>
+ </head>
+ <body>
+ <img src='firefox-logo-nightly.svg' width="24">
+ </body>
+</html>
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_register.html b/tools/profiler/tests/browser/serviceworkers/serviceworker_register.html
new file mode 100644
index 0000000000..86719787f4
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_register.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <script src='serviceworker-utils.js'></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_simple.html b/tools/profiler/tests/browser/serviceworkers/serviceworker_simple.html
new file mode 100644
index 0000000000..f7c32d02c3
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_simple.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ Testing
+ </body>
+</html>
diff --git a/tools/profiler/tests/browser/serviceworkers/serviceworker_synthetized_response.js b/tools/profiler/tests/browser/serviceworkers/serviceworker_synthetized_response.js
new file mode 100644
index 0000000000..891b679a5f
--- /dev/null
+++ b/tools/profiler/tests/browser/serviceworkers/serviceworker_synthetized_response.js
@@ -0,0 +1,27 @@
+self.addEventListener("install", event => {
+ performance.mark("__serviceworker_event");
+ dump("[SW]:", "Install event\n");
+});
+
+self.addEventListener("fetch", event => {
+ performance.mark("__serviceworker_event");
+ dump(`Handling fetch event for ${event.request.url}\n`);
+ event.respondWith(handleFetch(event.request));
+});
+
+async function handleFetch(request) {
+ if (request.url.endsWith("-generated.svg")) {
+ dump(
+ "An icon file that should be generated was requested, let's answer directly.\n"
+ );
+ return new Response(
+ `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 953.37 984"><defs><linearGradient id="linear-gradient" x1="-14706.28" y1="9250.14" x2="-14443.04" y2="9250.14" gradientTransform="matrix(0.76, 0.03, 0.05, -1.12, 11485.47, 11148)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.1" stop-color="#0092f8"/><stop offset="0.31" stop-color="#00abeb"/><stop offset="0.52" stop-color="#00bee1"/><stop offset="0.75" stop-color="#00c8dc"/><stop offset="1" stop-color="#00ccda"/></linearGradient><radialGradient id="radial-gradient" cx="-7588.66" cy="8866.53" r="791.23" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0.02" stop-color="#005fe7"/><stop offset="0.18" stop-color="#0042b4"/><stop offset="0.32" stop-color="#002989"/><stop offset="0.4" stop-color="#002079"/><stop offset="0.47" stop-color="#131d78"/><stop offset="0.66" stop-color="#3b1676"/><stop offset="0.75" stop-color="#4a1475"/></radialGradient><linearGradient id="linear-gradient-2" x1="539.64" y1="254.8" x2="348.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 1, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000f43" stop-opacity="0.4"/><stop offset="0.48" stop-color="#001962" stop-opacity="0.17"/><stop offset="1" stop-color="#002079" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-3" x1="540.64" y1="254.8" x2="349.2" y2="881.03" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" xlink:href="#linear-gradient-2"/><linearGradient id="linear-gradient-4" x1="-8367.12" y1="7348.87" x2="-8482.36" y2="7357.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#812cc9"/><stop offset="1" stop-color="#005fe7"/></linearGradient><linearGradient id="linear-gradient-5" x1="-8449.89" y1="7496.97" x2="-8341.94" y2="7609.09" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.05" stop-color="#005fe7"/><stop offset="0.18" stop-color="#065de6"/><stop offset="0.35" stop-color="#1856e1"/><stop offset="0.56" stop-color="#354adb"/><stop offset="0.78" stop-color="#5d3ad1"/><stop offset="0.95" stop-color="#812cc9"/></linearGradient><linearGradient id="linear-gradient-6" x1="-8653.41" y1="7245.3" x2="-8422.52" y2="7244.76" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#002079"/><stop offset="0.99" stop-color="#a238ff"/></linearGradient><radialGradient id="radial-gradient-2" cx="644.11" cy="599.83" fx="785.0454815336918" fy="470.6889181532662" r="793.95" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0.2" stop-color="#00fdff"/><stop offset="0.26" stop-color="#0af1ff"/><stop offset="0.37" stop-color="#23d2ff"/><stop offset="0.52" stop-color="#4da0ff"/><stop offset="0.69" stop-color="#855bff"/><stop offset="0.77" stop-color="#a238ff"/><stop offset="0.81" stop-color="#a738fd"/><stop offset="0.86" stop-color="#b539f9"/><stop offset="0.9" stop-color="#cd39f1"/><stop offset="0.96" stop-color="#ee3ae6"/><stop offset="0.98" stop-color="#ff3be0"/></radialGradient><linearGradient id="linear-gradient-7" x1="-7458.97" y1="9093.17" x2="-7531.06" y2="8282.84" gradientTransform="matrix(1.23, 0, 0, -1.22, 9958.21, 11048.11)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00"/><stop offset="0.1" stop-color="#00e244"/><stop offset="0.22" stop-color="#00d694"/><stop offset="0.31" stop-color="#00cfc7"/><stop offset="0.35" stop-color="#00ccda"/><stop offset="0.42" stop-color="#0bc2dd" stop-opacity="0.92"/><stop offset="0.57" stop-color="#29a7e4" stop-opacity="0.72"/><stop offset="0.77" stop-color="#597df0" stop-opacity="0.4"/><stop offset="1" stop-color="#9448ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-8" x1="-8926.61" y1="7680.53" x2="-8790.14" y2="7680.53" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#005fe7"/><stop offset="0.46" stop-color="#0071f3" stop-opacity="0.51"/><stop offset="0.83" stop-color="#007efc" stop-opacity="0.14"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><radialGradient id="radial-gradient-3" cx="-8914.62" cy="7721.05" r="165.97" gradientTransform="matrix(1.22, 0.12, 0.12, -1.22, 10241.06, 10765.32)" gradientUnits="userSpaceOnUse"><stop offset="0.63" stop-color="#ffe302" stop-opacity="0"/><stop offset="0.67" stop-color="#ffe302" stop-opacity="0.05"/><stop offset="0.75" stop-color="#ffe302" stop-opacity="0.19"/><stop offset="0.86" stop-color="#ffe302" stop-opacity="0.4"/><stop offset="0.99" stop-color="#ffe302" stop-opacity="0.7"/></radialGradient><linearGradient id="linear-gradient-9" x1="214.02" y1="2032.47" x2="96.19" y2="2284.31" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, -250.1, 2306.29)" gradientUnits="userSpaceOnUse"><stop offset="0.19" stop-color="#4a1475" stop-opacity="0.5"/><stop offset="0.62" stop-color="#2277ac" stop-opacity="0.23"/><stop offset="0.94" stop-color="#00ccda" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-10" x1="-38.44" y1="278.18" x2="55.67" y2="171.29" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0.01" stop-color="#002079" stop-opacity="0.5"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-11" x1="142.45" y1="96.25" x2="142.5" y2="149.68" gradientTransform="matrix(0.99, 0.1, 0.1, -0.99, 229.04, 745.87)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4a1475" stop-opacity="0.9"/><stop offset="0.18" stop-color="#6720a2" stop-opacity="0.6"/><stop offset="0.38" stop-color="#812acb" stop-opacity="0.34"/><stop offset="0.57" stop-color="#9332e8" stop-opacity="0.15"/><stop offset="0.76" stop-color="#9e36f9" stop-opacity="0.04"/><stop offset="0.93" stop-color="#a238ff" stop-opacity="0"/></linearGradient><linearGradient id="linear-gradient-12" x1="620.52" y1="947.88" x2="926.18" y2="264.39" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00ec00" stop-opacity="0"/><stop offset="0.28" stop-color="#00dc6d" stop-opacity="0.5"/><stop offset="0.5" stop-color="#00d1bb" stop-opacity="0.86"/><stop offset="0.6" stop-color="#00ccda"/><stop offset="0.68" stop-color="#04c9db"/><stop offset="0.75" stop-color="#0fc1df"/><stop offset="0.83" stop-color="#23b2e6"/><stop offset="0.9" stop-color="#3e9ef0"/><stop offset="0.98" stop-color="#6184fc"/><stop offset="0.99" stop-color="#6680fe"/></linearGradient><linearGradient id="linear-gradient-13" x1="680.88" y1="554.79" x2="536.1" y2="166.04" gradientTransform="matrix(1, 0, 0, -1, 0, 984)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0083ff"/><stop offset="0.04" stop-color="#0083ff" stop-opacity="0.92"/><stop offset="0.14" stop-color="#0083ff" stop-opacity="0.71"/><stop offset="0.26" stop-color="#0083ff" stop-opacity="0.52"/><stop offset="0.37" stop-color="#0083ff" stop-opacity="0.36"/><stop offset="0.49" stop-color="#0083ff" stop-opacity="0.23"/><stop offset="0.61" stop-color="#0083ff" stop-opacity="0.13"/><stop offset="0.73" stop-color="#0083ff" stop-opacity="0.06"/><stop offset="0.86" stop-color="#0083ff" stop-opacity="0.01"/><stop offset="1" stop-color="#0083ff" stop-opacity="0"/></linearGradient></defs><title>firefox-logo-nightly</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Layer_2-2" data-name="Layer 2"><g id="Firefox"><path d="M770.28,91.56c-23.95,27.88-35.1,90.64-10.82,154.26s61.5,49.8,84.7,114.67c30.62,85.6,16.37,200.59,16.37,200.59s36.81,106.61,62.47-6.63C979.79,341.74,770.28,143.94,770.28,91.56Z" style="fill:url(#linear-gradient)"/><path id="_Path_" data-name=" Path " d="M476.92,972.83c245.24,0,443.9-199.74,443.9-446s-198.66-446-443.66-446S33.5,280.51,33.5,526.8C33,773.33,231.92,972.83,476.92,972.83Z" style="fill:url(#radial-gradient)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-2)"/><path d="M810.67,803.64a246.8,246.8,0,0,1-30.12,18.18,705.31,705.31,0,0,0,38.3-63c9.46-10.47,18.13-20.65,25.19-31.65,3.44-5.41,7.31-12.08,11.42-19.82,24.92-44.9,52.4-117.56,53.18-192.2v-5.66a257.25,257.25,0,0,0-5.71-55.75c.2,1.43.38,2.86.56,4.29-.22-1.1-.41-2.21-.64-3.31.37,2,.66,4,1,6,5.09,43.22,1.47,85.37-16.68,116.45-.29.45-.58.88-.87,1.32,9.41-47.23,12.56-99.39,2.09-151.6,0,0-4.19-25.38-35.38-102.44-18-44.35-49.83-80.72-78-107.21-24.69-30.55-47.11-51-59.47-64.06C689.72,126,678.9,105.61,674.45,92.31c-3.85-1.93-53.14-49.81-57.05-51.63-21.51,33.35-89.16,137.67-57,235.15,14.58,44.17,51.47,90,90.07,115.74,1.69,1.94,23,25,33.09,77.16,10.45,53.85,5,95.86-16.54,158C641.73,681.24,577,735.12,516.3,740.63c-129.67,11.78-177.15-65.11-177.15-65.11C385.49,694,436.72,690.17,467.87,671c31.4-19.43,50.39-33.83,65.81-28.15C548.86,648.43,561,632,550.1,615a78.5,78.5,0,0,0-79.4-34.57c-31.43,5.11-60.23,30-101.41,5.89a86.29,86.29,0,0,1-7.73-5.06c-2.71-1.79,8.83,2.72,6.13.69-8-4.35-22.2-13.84-25.88-17.22-.61-.56,6.22,2.18,5.61,1.62-38.51-31.71-33.7-53.13-32.49-66.57,1-10.75,8-24.52,19.75-30.11,5.69,3.11,9.24,5.48,9.24,5.48s-2.43-5-3.74-7.58c.46-.2.9-.15,1.36-.34,4.66,2.25,15,8.1,20.41,11.67,7.07,5,9.33,9.44,9.33,9.44s1.86-1,.48-5.37c-.5-1.78-2.65-7.45-9.65-13.17h.44A81.61,81.61,0,0,1,374.42,478c2-7.18,5.53-14.68,4.75-28.09-.48-9.43-.26-11.87-1.92-15.51-1.49-3.13.83-4.35,3.42-1.1a32.5,32.5,0,0,0-2.21-7.4v-.24c3.23-11.24,68.25-40.46,73-43.88A67.2,67.2,0,0,0,470.59,361c3.62-5.76,6.34-13.85,7-26.11.36-8.84-3.76-14.73-69.51-21.62-18-1.77-28.53-14.8-34.53-26.82-1.09-2.59-2.21-4.94-3.33-7.28a57.68,57.68,0,0,1-2.56-8.43c10.75-30.87,28.81-57,55.37-76.7,1.45-1.32-5.78.34-4.34-1,1.69-1.54,12.71-6,14.79-7,2.54-1.2-10.88-6.9-22.73-5.51-12.07,1.36-14.63,2.8-21.07,5.53,2.67-2.66,11.17-6.15,9.18-6.13-13,2-29.18,9.56-43,18.12a10.66,10.66,0,0,1,.83-4.35c-6.44,2.73-22.26,13.79-26.87,23.14a44.29,44.29,0,0,0,.27-5.4,84.17,84.17,0,0,0-13.19,13.82l-.24.22c-37.36-15-70.23-16-98.05-9.28-6.09-6.11-9.06-1.64-22.91-32.07-.94-1.83.72,1.81,0,0-2.28-5.9,1.39,7.87,0,0-23.28,18.37-53.92,39.19-68.63,53.89-.18.59,17.16-4.9,0,0-6,1.72-5.6,5.28-6.51,37.5-.22,2.44,0,5.18-.22,7.38-11.75,15-19.75,27.64-22.78,34.21-15.19,26.18-31.93,67-48.15,131.55A334.82,334.82,0,0,1,75.2,398.36C61.71,432.63,48.67,486.44,46.07,569.3A482.08,482.08,0,0,1,58.6,518.64,473,473,0,0,0,93.33,719.71c9.33,22.82,24.76,57.46,51,95.4C226.9,902,343.31,956,472.21,956,606.79,956,727.64,897.13,810.67,803.64Z" style="fill:url(#linear-gradient-3)"/><path d="M711.1,866.71c162.87-18.86,235-186.7,142.38-190C769.85,674,634,875.61,711.1,866.71Z" style="fill:url(#linear-gradient-4)"/><path d="M865.21,642.42C977.26,577.21,948,436.34,948,436.34s-43.25,50.24-72.62,130.32C846.4,646,797.84,681.81,865.21,642.42Z" style="fill:url(#linear-gradient-5)"/><path d="M509.47,950.06C665.7,999.91,800,876.84,717.21,835.74,642,798.68,435.32,926.49,509.47,950.06Z" style="fill:url(#linear-gradient-6)"/><path d="M638.58,21.42l.53-.57A1.7,1.7,0,0,0,638.58,21.42ZM876.85,702.23c3.8-5.36,8.94-22.53,13.48-30.21,27.58-44.52,27.78-80,27.78-80.84,16.66-83.22,15.15-117.2,4.9-180-8.25-50.6-44.32-123.09-75.57-158-32.2-36-9.51-24.25-40.69-50.52-27.33-30.29-53.82-60.29-68.25-72.36C634.22,43.09,636.57,24.58,638.58,21.42c-.34.37-.84.92-1.47,1.64C635.87,18.14,635,14,635,14s-57,57-69,152c-7.83,62,15.38,126.68,49,168a381.62,381.62,0,0,0,59,58h0c25.4,36.48,39.38,81.49,39.38,129.91,0,121.24-98.34,219.53-219.65,219.53a220.14,220.14,0,0,1-49.13-5.52c-57.24-10.92-90.3-39.8-106.78-59.41-9.45-11.23-13.46-19.42-13.46-19.42,51.28,18.37,108,14.53,142.47-4.52,34.75-19.26,55.77-33.55,72.84-27.92,16.82,5.61,30.21-10.67,18.2-27.54-11.77-16.85-42.4-41-87.88-34.29-34.79,5.07-66.66,29.76-112.24,5.84a97.34,97.34,0,0,1-8.55-5c-3-1.77,9.77,2.69,6.79.68-8.87-4.32-24.57-13.73-28.64-17.07-.68-.56,6.88,2.16,6.2,1.6-42.62-31.45-37.3-52.69-36-66,1.07-10.66,8.81-24.32,21.86-29.86,6.3,3.08,10.23,5.43,10.23,5.43s-2.69-4.92-4.14-7.51c.51-.19,1-.15,1.5-.34,5.16,2.23,16.58,8,22.59,11.57,7.83,4.95,10.32,9.36,10.32,9.36s2.06-1,.54-5.33c-.56-1.77-2.93-7.39-10.68-13.07h.48a91.65,91.65,0,0,1,13.13,8.17c2.19-7.12,6.12-14.56,5.25-27.86-.53-9.35-.28-11.78-2.12-15.39-1.65-3.1.92-4.31,3.78-1.09a29.73,29.73,0,0,0-2.44-7.34v-.24c3.57-11.14,75.53-40.12,80.77-43.51a70.24,70.24,0,0,0,21.17-20.63c4-5.72,7-13.73,7.75-25.89.25-5.48-1.44-9.82-20.5-14-11.44-2.49-29.14-4.91-56.43-7.47-19.9-1.76-31.58-14.68-38.21-26.6-1.21-2.57-2.45-4.9-3.68-7.22a53.41,53.41,0,0,1-2.83-8.36,158.47,158.47,0,0,1,61.28-76.06c1.6-1.31-6.4.33-4.8-1,1.87-1.52,14.06-5.93,16.37-6.92,2.81-1.19-12-6.84-25.16-5.47-13.36,1.35-16.19,2.78-23.32,5.49,3-2.64,12.37-6.1,10.16-6.08-14.4,2-32.3,9.48-47.6,18a9.72,9.72,0,0,1,.92-4.31c-7.13,2.71-24.64,13.67-29.73,23a39.79,39.79,0,0,0,.29-5.35,88.55,88.55,0,0,0-14.6,13.7l-.27.22C258.14,196,221.75,195,191,201.72c-6.74-6.06-17.57-15.23-32.89-45.4-1-1.82-1.6,3.75-2.4,2-6-13.81-9.55-36.44-9-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.87-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.37,12.62-1.38,4-3.32,6.27-4.56,11.29l-.29.46c-.1-1.48.37-6.08,0-5.14A235.4,235.4,0,0,0,95.34,186c-5.49,18-11.88,42.61-12.89,74.57-.24,2.42,0,5.14-.25,7.32-13,14.83-21.86,27.39-25.2,33.91-16.81,26-35.33,66.44-53.29,130.46a319.35,319.35,0,0,1,28.54-50C17.32,416.25,2.89,469.62,0,551.8a436.92,436.92,0,0,1,13.87-50.24C11.29,556.36,17.68,624.3,52.32,701c20.57,45,67.92,136.6,183.62,208h0s39.36,29.3,107,51.26c5,1.81,10.06,3.6,15.23,5.33q-2.43-1-4.71-2A484.9,484.9,0,0,0,492.27,984c175.18.15,226.85-70.2,226.85-70.2l-.51.38q3.71-3.49,7.14-7.26c-27.64,26.08-90.75,27.84-114.3,26,40.22-11.81,66.69-21.81,118.17-41.52q9-3.36,18.48-7.64l2-.94c1.25-.58,2.49-1.13,3.75-1.74a349.3,349.3,0,0,0,70.26-44c51.7-41.3,63-81.56,68.83-108.1-.82,2.54-3.37,8.47-5.17,12.32-13.31,28.48-42.84,46-74.91,61a689.05,689.05,0,0,0,42.38-62.44C865.77,729.39,869,713.15,876.85,702.23Z" style="fill:url(#radial-gradient-2)"/><path d="M813.92,801c21.08-23.24,40-49.82,54.35-80,36.9-77.58,94-206.58,49-341.31C881.77,273.22,833,215,771.11,158.12,670.56,65.76,642.48,24.52,642.48,0c0,0-116.09,129.41-65.74,264.38s153.46,130,221.68,270.87c80.27,165.74-64.95,346.61-185,397.24,7.35-1.63,267-60.38,280.61-208.88C893.68,726.34,887.83,767.41,813.92,801Z" style="fill:url(#linear-gradient-7)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="fill:url(#linear-gradient-8)"/><path d="M477.59,319.37c.39-8.77-4.16-14.66-76.68-21.46-29.84-2.76-41.26-30.33-44.75-41.94-10.61,27.56-15,56.49-12.64,91.48,1.61,22.92,17,47.52,24.37,62,0,0,1.64-2.13,2.39-2.91,13.86-14.43,71.94-36.42,77.39-39.54C453.69,363.16,476.58,346.44,477.59,319.37Z" style="opacity:0.5;isolation:isolate;fill:url(#radial-gradient-3)"/><path d="M158.31,156.47c-1-1.82-1.6,3.75-2.4,2-6-13.81-9.58-36.2-8.72-52,0,0-12.32,5.61-22.51,29.06-1.89,4.21-3.11,6.54-4.32,8.86-.56.68,1.27-7.7,1-7.24-1.77,3-6.36,7.19-8.35,12.38-1.65,4.24-3.35,6.52-4.61,11.77-.39,1.43.39-6.32,0-5.38C84.72,201.68,80.19,271,82.69,268,133.17,214.14,191,201.36,191,201.36c-6.15-4.53-19.53-17.63-32.7-44.89Z" style="fill:url(#linear-gradient-9)"/><path d="M349.84,720.1c-69.72-29.77-149-71.75-146-167.14C207.92,427.35,321,452.18,321,452.18c-4.27,1-15.68,9.16-19.72,17.82-4.27,10.83-12.07,35.28,11.55,60.9,37.09,40.19-76.2,95.36,98.66,199.57,4.41,2.4-41-1.43-61.64-10.36Z" style="fill:url(#linear-gradient-10)"/><path d="M325.07,657.5c49.44,17.21,107,14.19,141.52-4.86,23.09-12.85,52.7-33.43,70.92-28.35-15.78-6.24-27.73-9.15-42.1-9.86-2.45,0-5.38,0-8-.32a136,136,0,0,0-15.76.86c-8.9.82-18.77,6.43-27.74,5.53-.48,0,8.7-3.77,8-3.61-4.75,1-9.92,1.21-15.37,1.88-3.47.39-6.45.82-9.89,1-103,8.73-190-55.81-190-55.81-7.41,25,33.17,74.3,88.52,93.57Z" style="opacity:0.5;isolation:isolate;fill:url(#linear-gradient-11)"/><path d="M813.74,801.65c104.16-102.27,156.86-226.58,134.58-366,0,0,8.9,71.5-24.85,144.63,16.21-71.39,18.1-160.11-25-252C841,205.64,746.45,141.11,710.35,114.19,655.66,73.4,633,31.87,632.57,23.3c-16.34,33.48-65.77,148.2-5.31,247,56.64,92.56,145.86,120,208.33,205C950.67,631.67,813.74,801.65,813.74,801.65Z" style="fill:url(#linear-gradient-12)"/><path d="M798.81,535.55C762.41,460.35,717,427.55,674,392c5,7,6.23,9.47,9,14,37.83,40.32,93.61,138.66,53.11,262.11C659.88,900.48,355,791.06,323,760.32,335.93,894.81,561,959.16,707.6,872,791,793,858.47,658.79,798.81,535.55Z" style="fill:url(#linear-gradient-13)"/></g></g></g></g></svg>`,
+ { headers: { "content-type": "image/svg+xml" } }
+ );
+ }
+
+ dump(
+ `A normal URL ${request.url} has been requested, let's fetch it from the network.\n`
+ );
+ return fetch(request);
+}
diff --git a/tools/profiler/tests/browser/simple.html b/tools/profiler/tests/browser/simple.html
new file mode 100644
index 0000000000..f7c32d02c3
--- /dev/null
+++ b/tools/profiler/tests/browser/simple.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ </head>
+ <body>
+ Testing
+ </body>
+</html>
diff --git a/tools/profiler/tests/browser/single_frame.html b/tools/profiler/tests/browser/single_frame.html
new file mode 100644
index 0000000000..ebdfc41da2
--- /dev/null
+++ b/tools/profiler/tests/browser/single_frame.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Single Frame</title>
+</head>
+<body>
+ Single Frame
+</body>
+</html>
diff --git a/tools/profiler/tests/chrome/chrome.ini b/tools/profiler/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..7089b8fb8e
--- /dev/null
+++ b/tools/profiler/tests/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+skip-if = tsan # Bug 1804081
+support-files=profiler_test_utils.js
+
+[test_profile_worker_bug_1428076.html]
+skip-if = os == 'android' && processor == 'arm' # Bug 1541291
+[test_profile_worker.html]
+skip-if = os == 'android' && processor == 'arm' # Bug 1541291
diff --git a/tools/profiler/tests/chrome/profiler_test_utils.js b/tools/profiler/tests/chrome/profiler_test_utils.js
new file mode 100644
index 0000000000..ae9970cbca
--- /dev/null
+++ b/tools/profiler/tests/chrome/profiler_test_utils.js
@@ -0,0 +1,66 @@
+"use strict";
+
+(function() {
+ async function startProfiler(settings) {
+ let startPromise = Services.profiler.StartProfiler(
+ settings.entries,
+ settings.interval,
+ settings.features,
+ settings.threads,
+ 0,
+ settings.duration
+ );
+
+ info("Parent Profiler has started");
+
+ await startPromise;
+
+ info("Child profilers have started");
+ }
+
+ function getProfile() {
+ const profile = Services.profiler.getProfileData();
+ info(
+ "We got a profile, run the mochitest with `--keep-open true` to see the logged profile in the Web Console."
+ );
+
+ // Run the mochitest with `--keep-open true` to see the logged profile in the
+ // Web console.
+ console.log(profile);
+
+ return profile;
+ }
+
+ async function stopProfiler() {
+ let stopPromise = Services.profiler.StopProfiler();
+ info("Parent profiler has stopped");
+ await stopPromise;
+ info("Child profilers have stopped");
+ }
+
+ function end(error) {
+ if (error) {
+ ok(false, `We got an error: ${error}`);
+ } else {
+ ok(true, "We ran the whole process");
+ }
+ SimpleTest.finish();
+ }
+
+ async function runTest(settings, workload) {
+ SimpleTest.waitForExplicitFinish();
+ try {
+ await startProfiler(settings);
+ await workload();
+ await getProfile();
+ await stopProfiler();
+ await end();
+ } catch (e) {
+ // By catching and handling the error, we're being nice to mochitest
+ // runners: instead of waiting for the timeout, we fail right away.
+ await end(e);
+ }
+ }
+
+ window.runTest = runTest;
+})();
diff --git a/tools/profiler/tests/chrome/test_profile_worker.html b/tools/profiler/tests/chrome/test_profile_worker.html
new file mode 100644
index 0000000000..8e2bae7fbd
--- /dev/null
+++ b/tools/profiler/tests/chrome/test_profile_worker.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1428076
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1428076</title>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428076">Mozilla Bug 1428076</a>
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="profiler_test_utils.js"></script>
+<script type="application/javascript">
+/* globals runTest */
+
+"use strict";
+
+const settings = {
+ entries: 1000000, // 9MB
+ interval: 1, // ms
+ features: ["js", "stackwalk", "cpu"],
+ threads: ["GeckoMain", "Compositor", "Worker"], // most common combination
+};
+
+const workerCode = `
+ console.log('hello world');
+ setTimeout(() => postMessage('message from worker'), 50);
+`;
+
+function startWorker() {
+ // We use a Blob for the worker content to avoid an external JS file, and data
+ // URLs seem to be blocked in a chrome environment.
+ const workerContent = new Blob(
+ [ workerCode ],
+ { type: "application/javascript" }
+ );
+ const blobURL = URL.createObjectURL(workerContent);
+
+ // We start a worker and then terminate it right away to trigger our bug.
+ info("Starting the worker...");
+ const myWorker = new Worker(blobURL);
+ return { worker: myWorker, url: blobURL };
+}
+
+function workload() {
+ const { worker, url } = startWorker();
+
+ return new Promise(resolve => {
+ worker.onmessage = () => {
+ info("Got a message, terminating the worker.");
+ worker.terminate();
+ URL.revokeObjectURL(url);
+ resolve();
+ };
+ });
+}
+
+runTest(settings, workload);
+
+</script>
+</body>
+</html>
diff --git a/tools/profiler/tests/chrome/test_profile_worker_bug_1428076.html b/tools/profiler/tests/chrome/test_profile_worker_bug_1428076.html
new file mode 100644
index 0000000000..abe0e5748a
--- /dev/null
+++ b/tools/profiler/tests/chrome/test_profile_worker_bug_1428076.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1428076
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1428076</title>
+ <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428076">Mozilla Bug 1428076</a>
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="profiler_test_utils.js"></script>
+<script type="application/javascript">
+/** Test for Bug 1428076 **/
+
+/* globals runTest */
+
+"use strict";
+
+const settings = {
+ entries: 1000000, // 9MB
+ interval: 1, // ms
+ features: ["js", "stackwalk"],
+ threads: ["GeckoMain", "Compositor", "Worker"], // most common combination
+};
+
+function workload() {
+ // We use a Blob for the worker content to avoid an external JS file, and data
+ // URLs seem to be blocked in a chrome environment.
+ const workerContent = new Blob(
+ [ "console.log('hello world!')" ],
+ { type: "application/javascript" }
+ );
+ const blobURL = URL.createObjectURL(workerContent);
+
+ // We start a worker and then terminate it right away to trigger our bug.
+ info("Starting the worker, and terminate it right away.");
+ const myWorker = new Worker(blobURL);
+ myWorker.terminate();
+
+ URL.revokeObjectURL(blobURL);
+
+ // We're deferring some little time so that the worker has the time to be
+ // properly cleaned up and the profiler actually saves the worker data.
+ return new Promise(resolve => {
+ setTimeout(resolve, 50);
+ });
+}
+
+runTest(settings, workload);
+
+</script>
+</body>
+</html>
diff --git a/tools/profiler/tests/gtest/GeckoProfiler.cpp b/tools/profiler/tests/gtest/GeckoProfiler.cpp
new file mode 100644
index 0000000000..1bf70bce09
--- /dev/null
+++ b/tools/profiler/tests/gtest/GeckoProfiler.cpp
@@ -0,0 +1,4974 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// This file tests a lot of the profiler_*() functions in GeckoProfiler.h.
+// Most of the tests just check that nothing untoward (e.g. crashes, deadlocks)
+// happens when calling these functions. They don't do much inspection of
+// profiler internals.
+
+#include "mozilla/ProfilerThreadPlatformData.h"
+#include "mozilla/ProfilerThreadRegistration.h"
+#include "mozilla/ProfilerThreadRegistrationInfo.h"
+#include "mozilla/ProfilerThreadRegistry.h"
+#include "mozilla/ProfilerUtils.h"
+#include "mozilla/ProgressLogger.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "prthread.h"
+
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+
+#include <thread>
+
+#if defined(_MSC_VER) || defined(__MINGW32__)
+# include <processthreadsapi.h>
+# include <realtimeapiset.h>
+#elif defined(__APPLE__)
+# include <mach/thread_act.h>
+#endif
+
+#ifdef MOZ_GECKO_PROFILER
+
+# include "GeckoProfiler.h"
+# include "mozilla/ProfilerMarkerTypes.h"
+# include "mozilla/ProfilerMarkers.h"
+# include "NetworkMarker.h"
+# include "platform.h"
+# include "ProfileBuffer.h"
+# include "ProfilerControl.h"
+
+# include "js/Initialization.h"
+# include "js/Printf.h"
+# include "jsapi.h"
+# include "json/json.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/BlocksRingBuffer.h"
+# include "mozilla/DataMutex.h"
+# include "mozilla/ProfileBufferEntrySerializationGeckoExtensions.h"
+# include "mozilla/ProfileJSONWriter.h"
+# include "mozilla/ScopeExit.h"
+# include "mozilla/net/HttpBaseChannel.h"
+# include "nsIChannelEventSink.h"
+# include "nsIThread.h"
+# include "nsThreadUtils.h"
+
+# include <cstring>
+# include <set>
+
+#endif // MOZ_GECKO_PROFILER
+
+// Note: profiler_init() has already been called in XRE_main(), so we can't
+// test it here. Likewise for profiler_shutdown(), and AutoProfilerInit
+// (which is just an RAII wrapper for profiler_init() and profiler_shutdown()).
+
+using namespace mozilla;
+
+TEST(GeckoProfiler, ProfilerUtils)
+{
+ profiler_init_main_thread_id();
+
+ static_assert(std::is_same_v<decltype(profiler_current_process_id()),
+ ProfilerProcessId>);
+ static_assert(
+ std::is_same_v<decltype(profiler_current_process_id()),
+ decltype(baseprofiler::profiler_current_process_id())>);
+ ProfilerProcessId processId = profiler_current_process_id();
+ EXPECT_TRUE(processId.IsSpecified());
+ EXPECT_EQ(processId, baseprofiler::profiler_current_process_id());
+
+ static_assert(
+ std::is_same_v<decltype(profiler_current_thread_id()), ProfilerThreadId>);
+ static_assert(
+ std::is_same_v<decltype(profiler_current_thread_id()),
+ decltype(baseprofiler::profiler_current_thread_id())>);
+ EXPECT_EQ(profiler_current_thread_id(),
+ baseprofiler::profiler_current_thread_id());
+
+ ProfilerThreadId mainTestThreadId = profiler_current_thread_id();
+ EXPECT_TRUE(mainTestThreadId.IsSpecified());
+
+ ProfilerThreadId mainThreadId = profiler_main_thread_id();
+ EXPECT_TRUE(mainThreadId.IsSpecified());
+
+ EXPECT_EQ(mainThreadId, mainTestThreadId)
+ << "Test should run on the main thread";
+ EXPECT_TRUE(profiler_is_main_thread());
+
+ std::thread testThread([&]() {
+ EXPECT_EQ(profiler_current_process_id(), processId);
+
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+ EXPECT_TRUE(testThreadId.IsSpecified());
+ EXPECT_NE(testThreadId, mainThreadId);
+ EXPECT_FALSE(profiler_is_main_thread());
+
+ EXPECT_EQ(baseprofiler::profiler_current_process_id(), processId);
+ EXPECT_EQ(baseprofiler::profiler_current_thread_id(), testThreadId);
+ EXPECT_EQ(baseprofiler::profiler_main_thread_id(), mainThreadId);
+ EXPECT_FALSE(baseprofiler::profiler_is_main_thread());
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistrationInfo)
+{
+ profiler_init_main_thread_id();
+
+ TimeStamp ts = TimeStamp::Now();
+ {
+ profiler::ThreadRegistrationInfo trInfo{
+ "name", ProfilerThreadId::FromNumber(123), false, ts};
+ EXPECT_STREQ(trInfo.Name(), "name");
+ EXPECT_NE(trInfo.Name(), "name")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ EXPECT_EQ(trInfo.RegisterTime(), ts);
+ EXPECT_EQ(trInfo.ThreadId(), ProfilerThreadId::FromNumber(123));
+ EXPECT_EQ(trInfo.IsMainThread(), false);
+ }
+
+ // Make sure the next timestamp will be different from `ts`.
+ while (TimeStamp::Now() == ts) {
+ }
+
+ {
+ profiler::ThreadRegistrationInfo trInfoHere{"Here"};
+ EXPECT_STREQ(trInfoHere.Name(), "Here");
+ EXPECT_NE(trInfoHere.Name(), "Here")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ TimeStamp baseRegistrationTime =
+ baseprofiler::detail::GetThreadRegistrationTime();
+ if (baseRegistrationTime) {
+ EXPECT_EQ(trInfoHere.RegisterTime(), baseRegistrationTime);
+ } else {
+ EXPECT_GT(trInfoHere.RegisterTime(), ts);
+ }
+ EXPECT_EQ(trInfoHere.ThreadId(), profiler_current_thread_id());
+ EXPECT_EQ(trInfoHere.ThreadId(), profiler_main_thread_id())
+ << "Gtests are assumed to run on the main thread";
+ EXPECT_EQ(trInfoHere.IsMainThread(), true)
+ << "Gtests are assumed to run on the main thread";
+ }
+
+ {
+ // Sub-thread test.
+ // These will receive sub-thread data (to test move at thread end).
+ TimeStamp tsThread;
+ ProfilerThreadId threadThreadId;
+ UniquePtr<profiler::ThreadRegistrationInfo> trInfoThreadPtr;
+
+ std::thread testThread([&]() {
+ profiler::ThreadRegistrationInfo trInfoThread{"Thread"};
+ EXPECT_STREQ(trInfoThread.Name(), "Thread");
+ EXPECT_NE(trInfoThread.Name(), "Thread")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ EXPECT_GT(trInfoThread.RegisterTime(), ts);
+ EXPECT_EQ(trInfoThread.ThreadId(), profiler_current_thread_id());
+ EXPECT_NE(trInfoThread.ThreadId(), profiler_main_thread_id());
+ EXPECT_EQ(trInfoThread.IsMainThread(), false);
+
+ tsThread = trInfoThread.RegisterTime();
+ threadThreadId = trInfoThread.ThreadId();
+ trInfoThreadPtr =
+ MakeUnique<profiler::ThreadRegistrationInfo>(std::move(trInfoThread));
+ });
+ testThread.join();
+
+ ASSERT_NE(trInfoThreadPtr, nullptr);
+ EXPECT_STREQ(trInfoThreadPtr->Name(), "Thread");
+ EXPECT_EQ(trInfoThreadPtr->RegisterTime(), tsThread);
+ EXPECT_EQ(trInfoThreadPtr->ThreadId(), threadThreadId);
+ EXPECT_EQ(trInfoThreadPtr->IsMainThread(), false)
+ << "Gtests are assumed to run on the main thread";
+ }
+}
+
+static constexpr ThreadProfilingFeatures scEachAndAnyThreadProfilingFeatures[] =
+ {ThreadProfilingFeatures::CPUUtilization, ThreadProfilingFeatures::Sampling,
+ ThreadProfilingFeatures::Markers, ThreadProfilingFeatures::Any};
+
+TEST(GeckoProfiler, ThreadProfilingFeaturesType)
+{
+ ASSERT_EQ(static_cast<uint32_t>(ThreadProfilingFeatures::Any), 1u + 2u + 4u)
+ << "This test assumes that there are 3 binary choices 1+2+4; "
+ "Is this test up to date?";
+
+ EXPECT_EQ(Combine(ThreadProfilingFeatures::CPUUtilization,
+ ThreadProfilingFeatures::Sampling,
+ ThreadProfilingFeatures::Markers),
+ ThreadProfilingFeatures::Any);
+
+ constexpr ThreadProfilingFeatures allThreadProfilingFeatures[] = {
+ ThreadProfilingFeatures::NotProfiled,
+ ThreadProfilingFeatures::CPUUtilization,
+ ThreadProfilingFeatures::Sampling, ThreadProfilingFeatures::Markers,
+ ThreadProfilingFeatures::Any};
+
+ for (ThreadProfilingFeatures f1 : allThreadProfilingFeatures) {
+ // Combine and Intersect are commutative.
+ for (ThreadProfilingFeatures f2 : allThreadProfilingFeatures) {
+ EXPECT_EQ(Combine(f1, f2), Combine(f2, f1));
+ EXPECT_EQ(Intersect(f1, f2), Intersect(f2, f1));
+ }
+
+ // Combine works like OR.
+ EXPECT_EQ(Combine(f1, f1), f1);
+ EXPECT_EQ(Combine(f1, f1, f1), f1);
+
+ // 'OR NotProfiled' doesn't change anything.
+ EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::NotProfiled), f1);
+
+ // 'OR Any' makes Any.
+ EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::Any),
+ ThreadProfilingFeatures::Any);
+
+ // Intersect works like AND.
+ EXPECT_EQ(Intersect(f1, f1), f1);
+ EXPECT_EQ(Intersect(f1, f1, f1), f1);
+
+ // 'AND NotProfiled' erases anything.
+ EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::NotProfiled),
+ ThreadProfilingFeatures::NotProfiled);
+
+ // 'AND Any' doesn't change anything.
+ EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::Any), f1);
+ }
+
+ for (ThreadProfilingFeatures f1 : scEachAndAnyThreadProfilingFeatures) {
+ EXPECT_TRUE(DoFeaturesIntersect(f1, f1));
+
+ // NotProfiled doesn't intersect with any feature.
+ EXPECT_FALSE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::NotProfiled));
+
+ // Any intersects with any feature.
+ EXPECT_TRUE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::Any));
+ }
+}
+
+static void TestConstUnlockedConstReader(
+ const profiler::ThreadRegistration::UnlockedConstReader& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ EXPECT_STREQ(aData.Info().Name(), "Test thread");
+ EXPECT_GE(aData.Info().RegisterTime(), aBeforeRegistration);
+ EXPECT_LE(aData.Info().RegisterTime(), aAfterRegistration);
+ EXPECT_EQ(aData.Info().ThreadId(), aThreadId);
+ EXPECT_FALSE(aData.Info().IsMainThread());
+
+#if (defined(_MSC_VER) || defined(__MINGW32__)) && defined(MOZ_GECKO_PROFILER)
+ HANDLE threadHandle = aData.PlatformDataCRef().ProfiledThread();
+ EXPECT_NE(threadHandle, nullptr);
+ EXPECT_EQ(ProfilerThreadId::FromNumber(::GetThreadId(threadHandle)),
+ aThreadId);
+ // Test calling QueryThreadCycleTime, we cannot assume that it will always
+ // work, but at least it shouldn't crash.
+ ULONG64 cycles;
+ (void)QueryThreadCycleTime(threadHandle, &cycles);
+#elif defined(__APPLE__) && defined(MOZ_GECKO_PROFILER)
+ // Test calling thread_info, we cannot assume that it will always work, but at
+ // least it shouldn't crash.
+ thread_basic_info_data_t threadBasicInfo;
+ mach_msg_type_number_t basicCount = THREAD_BASIC_INFO_COUNT;
+ (void)thread_info(
+ aData.PlatformDataCRef().ProfiledThread(), THREAD_BASIC_INFO,
+ reinterpret_cast<thread_info_t>(&threadBasicInfo), &basicCount);
+#elif (defined(__linux__) || defined(__ANDROID__) || defined(__FreeBSD__)) && \
+ defined(MOZ_GECKO_PROFILER)
+ // Test calling GetClockId, we cannot assume that it will always work, but at
+ // least it shouldn't crash.
+ Maybe<clockid_t> maybeClockId = aData.PlatformDataCRef().GetClockId();
+ if (maybeClockId) {
+ // Test calling clock_gettime, we cannot assume that it will always work,
+ // but at least it shouldn't crash.
+ timespec ts;
+ (void)clock_gettime(*maybeClockId, &ts);
+ }
+#else
+ (void)aData.PlatformDataCRef();
+#endif
+
+ EXPECT_GE(aData.StackTop(), aOnStackObject)
+ << "StackTop should be at &onStackChar, or higher on some "
+ "platforms";
+};
+
+static void TestConstUnlockedConstReaderAndAtomicRW(
+ const profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReader(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ (void)aData.ProfilingStackCRef();
+
+ EXPECT_EQ(aData.ProfilingFeatures(), ThreadProfilingFeatures::NotProfiled);
+
+ EXPECT_FALSE(aData.IsSleeping());
+};
+
+static void TestUnlockedConstReaderAndAtomicRW(
+ profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ (void)aData.ProfilingStackRef();
+
+ EXPECT_FALSE(aData.IsSleeping());
+ aData.SetSleeping();
+ EXPECT_TRUE(aData.IsSleeping());
+ aData.SetAwake();
+ EXPECT_FALSE(aData.IsSleeping());
+
+ aData.ReinitializeOnResume();
+
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.SetSleeping();
+ // After sleeping, the 2nd+ calls can duplicate.
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.ReinitializeOnResume();
+ // After reinit (and sleeping), the 2nd+ calls can duplicate.
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.SetAwake();
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+};
+
+static void TestConstUnlockedRWForLockedProfiler(
+ const profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // We can't create a PSAutoLock here, so just verify that the call would
+ // compile and return the expected type.
+ static_assert(std::is_same_v<decltype(aData.GetProfiledThreadData(
+ std::declval<PSAutoLock>())),
+ const ProfiledThreadData*>);
+};
+
+static void TestConstUnlockedReaderAndAtomicRWOnThread(
+ const profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread&
+ aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ EXPECT_EQ(aData.GetJSContext(), nullptr);
+};
+
+static void TestUnlockedRWForLockedProfiler(
+ profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+ TestUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestUnlockedReaderAndAtomicRWOnThread(
+ profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+ TestUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestConstLockedRWFromAnyThread(
+ const profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ EXPECT_EQ(aData.GetJsFrameBuffer(), nullptr);
+ EXPECT_EQ(aData.GetEventTarget(), nullptr);
+};
+
+static void TestLockedRWFromAnyThread(
+ profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+ TestUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // We can't create a ProfiledThreadData nor PSAutoLock here, so just verify
+ // that the call would compile and return the expected type.
+ static_assert(std::is_same_v<decltype(aData.SetProfilingFeaturesAndData(
+ std::declval<ThreadProfilingFeatures>(),
+ std::declval<ProfiledThreadData*>(),
+ std::declval<PSAutoLock>())),
+ void>);
+
+ aData.ResetMainThread(nullptr);
+
+ TimeDuration delay = TimeDuration::FromSeconds(1);
+ TimeDuration running = TimeDuration::FromSeconds(1);
+ aData.GetRunningEventDelay(TimeStamp::Now(), delay, running);
+ EXPECT_TRUE(delay.IsZero());
+ EXPECT_TRUE(running.IsZero());
+
+ aData.StartJSSampling(123u);
+ aData.StopJSSampling();
+};
+
+static void TestConstLockedRWOnThread(
+ const profiler::ThreadRegistration::LockedRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestLockedRWOnThread(
+ profiler::ThreadRegistration::LockedRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWOnThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+ TestLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ // We don't want to really call SetJSContext here, so just verify that
+ // the call would compile and return the expected type.
+ static_assert(
+ std::is_same_v<decltype(aData.SetJSContext(std::declval<JSContext*>())),
+ void>);
+ aData.ClearJSContext();
+ aData.PollJSSampling();
+};
+
+TEST(GeckoProfiler, ThreadRegistration_DataAccess)
+{
+ using TR = profiler::ThreadRegistration;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TimeStamp beforeRegistration = TimeStamp::Now();
+ TR tr{"Test thread", &onStackChar};
+ TimeStamp afterRegistration = TimeStamp::Now();
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ // Note: This test will mostly be about checking the correct access to
+ // thread data, depending on how it's obtained. Not all the functionality
+ // related to that data is tested (e.g., because it involves JS or other
+ // external dependencies that would be difficult to control here.)
+
+ auto TestOnThreadRef = [&](TR::OnThreadRef aOnThreadRef) {
+ // To test const-qualified member functions.
+ const TR::OnThreadRef& onThreadCRef = aOnThreadRef;
+
+ // const UnlockedConstReader (always const)
+
+ TestConstUnlockedConstReader(onThreadCRef.UnlockedConstReaderCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ onThreadCRef.WithUnlockedConstReader(
+ [&](const TR::UnlockedConstReader& aData) {
+ TestConstUnlockedConstReader(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedConstReaderAndAtomicRW
+
+ TestConstUnlockedConstReaderAndAtomicRW(
+ onThreadCRef.UnlockedConstReaderAndAtomicRWCRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](const TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestConstUnlockedConstReaderAndAtomicRW(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedConstReaderAndAtomicRW
+
+ TestUnlockedConstReaderAndAtomicRW(
+ aOnThreadRef.UnlockedConstReaderAndAtomicRWRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedRWForLockedProfiler
+
+ TestConstUnlockedRWForLockedProfiler(
+ onThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedRWForLockedProfiler(
+ [&](const TR::UnlockedRWForLockedProfiler& aData) {
+ TestConstUnlockedRWForLockedProfiler(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedRWForLockedProfiler
+
+ TestUnlockedRWForLockedProfiler(
+ aOnThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedRWForLockedProfiler(
+ [&](TR::UnlockedRWForLockedProfiler& aData) {
+ TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedReaderAndAtomicRWOnThread
+
+ TestConstUnlockedReaderAndAtomicRWOnThread(
+ onThreadCRef.UnlockedReaderAndAtomicRWOnThreadCRef(),
+ beforeRegistration, afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedReaderAndAtomicRWOnThread(
+ [&](const TR::UnlockedReaderAndAtomicRWOnThread& aData) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedReaderAndAtomicRWOnThread
+
+ TestUnlockedReaderAndAtomicRWOnThread(
+ aOnThreadRef.UnlockedReaderAndAtomicRWOnThreadRef(),
+ beforeRegistration, afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedReaderAndAtomicRWOnThread(
+ [&](TR::UnlockedReaderAndAtomicRWOnThread& aData) {
+ TestUnlockedReaderAndAtomicRWOnThread(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // LockedRWFromAnyThread
+ // Note: It cannot directly be accessed on the thread, this will be
+ // tested through LockedRWOnThread.
+
+ // const LockedRWOnThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TR::OnThreadRef::ConstRWOnThreadWithLock constRWOnThreadWithLock =
+ onThreadCRef.ConstLockedRWOnThread();
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(constRWOnThreadWithLock.DataCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ onThreadCRef.WithConstLockedRWOnThread(
+ [&](const TR::LockedRWOnThread& aData) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // non-const LockedRWOnThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TR::OnThreadRef::RWOnThreadWithLock rwOnThreadWithLock =
+ aOnThreadRef.LockedRWOnThread();
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(rwOnThreadWithLock.DataCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ TestLockedRWOnThread(rwOnThreadWithLock.DataRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ aOnThreadRef.WithLockedRWOnThread([&](TR::LockedRWOnThread& aData) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestLockedRWOnThread(aData, beforeRegistration, afterRegistration,
+ &onStackChar);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ };
+
+ TR::OnThreadPtr onThreadPtr = TR::GetOnThreadPtr();
+ ASSERT_TRUE(onThreadPtr);
+ TestOnThreadRef(*onThreadPtr);
+
+ TR::WithOnThreadRef(
+ [&](TR::OnThreadRef aOnThreadRef) { TestOnThreadRef(aOnThreadRef); });
+
+ EXPECT_TRUE(TR::WithOnThreadRefOr(
+ [&](TR::OnThreadRef aOnThreadRef) {
+ TestOnThreadRef(aOnThreadRef);
+ return true;
+ },
+ false));
+ });
+ testThread.join();
+}
+
+// Thread name if registered, nullptr otherwise.
+static const char* GetThreadName() {
+ return profiler::ThreadRegistration::WithOnThreadRefOr(
+ [](profiler::ThreadRegistration::OnThreadRef onThreadRef) {
+ return onThreadRef.WithUnlockedConstReader(
+ [](const profiler::ThreadRegistration::UnlockedConstReader& aData) {
+ return aData.Info().Name();
+ });
+ },
+ nullptr);
+}
+
+// Get the thread name, as registered in the PRThread, nullptr on failure.
+static const char* GetPRThreadName() {
+ nsIThread* nsThread = NS_GetCurrentThread();
+ if (!nsThread) {
+ return nullptr;
+ }
+ PRThread* prThread = nullptr;
+ if (NS_FAILED(nsThread->GetPRThread(&prThread))) {
+ return nullptr;
+ }
+ if (!prThread) {
+ return nullptr;
+ }
+ return PR_GetThreadName(prThread);
+}
+
+TEST(GeckoProfiler, ThreadRegistration_MainThreadName)
+{
+ EXPECT_TRUE(profiler::ThreadRegistration::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "GeckoMain");
+
+ // Check that the real thread name (outside the profiler) is *not* GeckoMain.
+ EXPECT_STRNE(GetPRThreadName(), "GeckoMain");
+}
+
+TEST(GeckoProfiler, ThreadRegistration_NestedRegistrations)
+{
+ using TR = profiler::ThreadRegistration;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test actual registrations that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+
+ char onStackChar;
+
+ // Blocks {} are mostly for clarity, but some control on-stack registration
+ // lifetimes.
+
+ // On-stack registration.
+ {
+ TR rt{"Test thread #1", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #1");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #1");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Off-stack registration.
+ {
+ TR::RegisterThread("Test thread #2", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #2");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #2");
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Extra un-registration should be ignored.
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested on-stack.
+ {
+ TR rt2{"Test thread #3", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #3");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3");
+
+ {
+ TR rt3{"Test thread #4", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #3")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3")
+ << "Nested registration shouldn't change the PRThread name";
+ }
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #3")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested off-stack.
+ {
+ TR::RegisterThread("Test thread #5", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #5");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5");
+
+ {
+ TR::RegisterThread("Test thread #6", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #5")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5")
+ << "Nested registration shouldn't change the PRThread name";
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #5")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5");
+ }
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Nested on- and off-stack.
+ {
+ TR rt2{"Test thread #7", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #7");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7");
+
+ {
+ TR::RegisterThread("Test thread #8", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #7")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7")
+ << "Nested registration shouldn't change the PRThread name";
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #7")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7");
+ }
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested off- and on-stack.
+ {
+ TR::RegisterThread("Test thread #9", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #9");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9");
+
+ {
+ TR rt3{"Test thread #10", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #9")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9")
+ << "Nested registration shouldn't change the PRThread name";
+ }
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #9")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9");
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Excess UnregisterThread with on-stack TR.
+ {
+ TR rt2{"Test thread #11", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #11");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #11");
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "On-stack thread should still be registered after off-stack "
+ "un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #11")
+ << "On-stack thread should still be registered after off-stack "
+ "un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #11");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Excess on-thread TR destruction with already-unregistered root off-thread
+ // registration.
+ {
+ TR::RegisterThread("Test thread #12", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #12");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #12");
+
+ {
+ TR rt3{"Test thread #13", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #12")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #12")
+ << "Nested registration shouldn't change the PRThread name";
+
+ // Note that we unregister the root registration, while nested `rt3` is
+ // still alive.
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered())
+ << "UnregisterThread() of the root RegisterThread() should always work";
+
+ // At this end of this block, `rt3` will be destroyed, but nothing
+ // should happen.
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ ASSERT_FALSE(TR::IsRegistered());
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistry_DataAccess)
+{
+ using TR = profiler::ThreadRegistration;
+ using TRy = profiler::ThreadRegistry;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TimeStamp beforeRegistration = TimeStamp::Now();
+ TR tr{"Test thread", &onStackChar};
+ TimeStamp afterRegistration = TimeStamp::Now();
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ // Note: This test will mostly be about checking the correct access to
+ // thread data, depending on how it's obtained. Not all the functionality
+ // related to that data is tested (e.g., because it involves JS or other
+ // external dependencies that would be difficult to control here.)
+
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+
+ auto testThroughRegistry = [&]() {
+ auto TestOffThreadRef = [&](TRy::OffThreadRef aOffThreadRef) {
+ // To test const-qualified member functions.
+ const TRy::OffThreadRef& offThreadCRef = aOffThreadRef;
+
+ // const UnlockedConstReader (always const)
+
+ TestConstUnlockedConstReader(offThreadCRef.UnlockedConstReaderCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedConstReader(
+ [&](const TR::UnlockedConstReader& aData) {
+ TestConstUnlockedConstReader(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // const UnlockedConstReaderAndAtomicRW
+
+ TestConstUnlockedConstReaderAndAtomicRW(
+ offThreadCRef.UnlockedConstReaderAndAtomicRWCRef(),
+ beforeRegistration, afterRegistration, &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](const TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestConstUnlockedConstReaderAndAtomicRW(
+ aData, beforeRegistration, afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // non-const UnlockedConstReaderAndAtomicRW
+
+ TestUnlockedConstReaderAndAtomicRW(
+ aOffThreadRef.UnlockedConstReaderAndAtomicRWRef(),
+ beforeRegistration, afterRegistration, &onStackChar, testThreadId);
+ aOffThreadRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
+ afterRegistration,
+ &onStackChar, testThreadId);
+ });
+
+ // const UnlockedRWForLockedProfiler
+
+ TestConstUnlockedRWForLockedProfiler(
+ offThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedRWForLockedProfiler(
+ [&](const TR::UnlockedRWForLockedProfiler& aData) {
+ TestConstUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration,
+ &onStackChar, testThreadId);
+ });
+
+ // non-const UnlockedRWForLockedProfiler
+
+ TestUnlockedRWForLockedProfiler(
+ aOffThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ aOffThreadRef.WithUnlockedRWForLockedProfiler(
+ [&](TR::UnlockedRWForLockedProfiler& aData) {
+ TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // UnlockedReaderAndAtomicRWOnThread
+ // Note: It cannot directly be accessed off the thread, this will be
+ // tested through LockedRWFromAnyThread.
+
+ // const LockedRWFromAnyThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TRy::OffThreadRef::ConstRWFromAnyThreadWithLock
+ constRWFromAnyThreadWithLock =
+ offThreadCRef.ConstLockedRWFromAnyThread();
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestConstLockedRWFromAnyThread(
+ constRWFromAnyThreadWithLock.DataCRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ offThreadCRef.WithConstLockedRWFromAnyThread(
+ [&](const TR::LockedRWFromAnyThread& aData) {
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestConstLockedRWFromAnyThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // non-const LockedRWFromAnyThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
+ aOffThreadRef.LockedRWFromAnyThread();
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestLockedRWFromAnyThread(rwFromAnyThreadWithLock.DataRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar, testThreadId);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ aOffThreadRef.WithLockedRWFromAnyThread(
+ [&](TR::LockedRWFromAnyThread& aData) {
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestLockedRWFromAnyThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // LockedRWOnThread
+ // Note: It can never be accessed off the thread.
+ };
+
+ int ranTest = 0;
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef aOffThreadRef) {
+ TestOffThreadRef(aOffThreadRef);
+ ++ranTest;
+ });
+ EXPECT_EQ(ranTest, 1);
+
+ EXPECT_TRUE(TRy::WithOffThreadRefOr(
+ testThreadId,
+ [&](TRy::OffThreadRef aOffThreadRef) {
+ TestOffThreadRef(aOffThreadRef);
+ return true;
+ },
+ false));
+
+ ranTest = 0;
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ for (TRy::OffThreadRef offThreadRef : TRy::LockedRegistry{}) {
+ EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
+ !TR::IsRegistered());
+ if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
+ testThreadId) {
+ TestOffThreadRef(offThreadRef);
+ ++ranTest;
+ }
+ }
+ EXPECT_EQ(ranTest, 1);
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+
+ {
+ ranTest = 0;
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ TRy::LockedRegistry lockedRegistry{};
+ EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
+ !TR::IsRegistered());
+ for (TRy::OffThreadRef offThreadRef : lockedRegistry) {
+ if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
+ testThreadId) {
+ TestOffThreadRef(offThreadRef);
+ ++ranTest;
+ }
+ }
+ EXPECT_EQ(ranTest, 1);
+ }
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ };
+
+ // Test on the current thread.
+ testThroughRegistry();
+
+ // Test from another thread.
+ std::thread otherThread([&]() {
+ ASSERT_NE(profiler_current_thread_id(), testThreadId);
+ testThroughRegistry();
+
+ // Test that this unregistered thread is really not registered.
+ int ranTest = 0;
+ TRy::WithOffThreadRef(
+ profiler_current_thread_id(),
+ [&](TRy::OffThreadRef aOffThreadRef) { ++ranTest; });
+ EXPECT_EQ(ranTest, 0);
+
+ EXPECT_FALSE(TRy::WithOffThreadRefOr(
+ profiler_current_thread_id(),
+ [&](TRy::OffThreadRef aOffThreadRef) {
+ ++ranTest;
+ return true;
+ },
+ false));
+ EXPECT_EQ(ranTest, 0);
+ });
+ otherThread.join();
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistration_RegistrationEdgeCases)
+{
+ using TR = profiler::ThreadRegistration;
+ using TRy = profiler::ThreadRegistry;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ int registrationCount = 0;
+ int otherThreadLoops = 0;
+ int otherThreadReads = 0;
+
+ // This thread will register and unregister in a loop, with some pauses.
+ // Another thread will attempty to access the test thread, and lock its data.
+ // The main goal is to check edges cases around (un)registrations.
+ std::thread testThread([&]() {
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+
+ const TimeStamp endTestAt = TimeStamp::Now() + TimeDuration::FromSeconds(1);
+
+ std::thread otherThread([&]() {
+ // Initial sleep so that testThread can start its loop.
+ PR_Sleep(PR_MillisecondsToInterval(1));
+
+ while (TimeStamp::Now() < endTestAt) {
+ ++otherThreadLoops;
+
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef
+ aOffThreadRef) {
+ if (otherThreadLoops % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
+ aOffThreadRef.LockedRWFromAnyThread();
+ ++otherThreadReads;
+ if (otherThreadReads % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+ }
+ });
+
+ while (TimeStamp::Now() < endTestAt) {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TR tr{"Test thread", &onStackChar};
+ ++registrationCount;
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ int ranTest = 0;
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef aOffThreadRef) {
+ if (registrationCount % 2000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ ++ranTest;
+ });
+ EXPECT_EQ(ranTest, 1);
+
+ if (registrationCount % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ }
+
+ otherThread.join();
+ });
+
+ testThread.join();
+
+ // It's difficult to guess what these numbers should be, but they definitely
+ // should be non-zero. The main goal was to test that nothing goes wrong.
+ EXPECT_GT(registrationCount, 0);
+ EXPECT_GT(otherThreadLoops, 0);
+ EXPECT_GT(otherThreadReads, 0);
+}
+
+#ifdef MOZ_GECKO_PROFILER
+
+TEST(BaseProfiler, BlocksRingBuffer)
+{
+ constexpr uint32_t MBSize = 256;
+ uint8_t buffer[MBSize * 3];
+ for (size_t i = 0; i < MBSize * 3; ++i) {
+ buffer[i] = uint8_t('A' + i);
+ }
+ BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex,
+ &buffer[MBSize], MakePowerOfTwo32<MBSize>());
+
+ {
+ nsCString cs("nsCString"_ns);
+ nsString s(u"nsString"_ns);
+ nsAutoCString acs("nsAutoCString"_ns);
+ nsAutoString as(u"nsAutoString"_ns);
+ nsAutoCStringN<8> acs8("nsAutoCStringN"_ns);
+ nsAutoStringN<8> as8(u"nsAutoStringN"_ns);
+ JS::UniqueChars jsuc = JS_smprintf("%s", "JS::UniqueChars");
+
+ rb.PutObjects(cs, s, acs, as, acs8, as8, jsuc);
+ }
+
+ rb.ReadEach([](ProfileBufferEntryReader& aER) {
+ ASSERT_EQ(aER.ReadObject<nsCString>(), "nsCString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsString>(), u"nsString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoCString>(), "nsAutoCString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoString>(), u"nsAutoString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoCStringN<8>>(), "nsAutoCStringN"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoStringN<8>>(), u"nsAutoStringN"_ns);
+ auto jsuc2 = aER.ReadObject<JS::UniqueChars>();
+ ASSERT_TRUE(!!jsuc2);
+ ASSERT_TRUE(strcmp(jsuc2.get(), "JS::UniqueChars") == 0);
+ });
+
+ // Everything around the sub-buffer should be unchanged.
+ for (size_t i = 0; i < MBSize; ++i) {
+ ASSERT_EQ(buffer[i], uint8_t('A' + i));
+ }
+ for (size_t i = MBSize * 2; i < MBSize * 3; ++i) {
+ ASSERT_EQ(buffer[i], uint8_t('A' + i));
+ }
+}
+
+// Common JSON checks.
+
+// Check that the given JSON string include no JSON whitespace characters
+// (excluding those in property names and strings).
+void JSONWhitespaceCheck(const char* aOutput) {
+ ASSERT_NE(aOutput, nullptr);
+
+ enum class State { Data, String, StringEscaped };
+ State state = State::Data;
+ size_t length = 0;
+ size_t whitespaces = 0;
+ for (const char* p = aOutput; *p != '\0'; ++p) {
+ ++length;
+ const char c = *p;
+
+ switch (state) {
+ case State::Data:
+ if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
+ ++whitespaces;
+ } else if (c == '"') {
+ state = State::String;
+ }
+ break;
+
+ case State::String:
+ if (c == '"') {
+ state = State::Data;
+ } else if (c == '\\') {
+ state = State::StringEscaped;
+ }
+ break;
+
+ case State::StringEscaped:
+ state = State::String;
+ break;
+ }
+ }
+
+ EXPECT_EQ(whitespaces, 0u);
+ EXPECT_GT(length, 0u);
+}
+
+// Does the GETTER return a non-null TYPE? (Non-critical)
+# define EXPECT_HAS_JSON(GETTER, TYPE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).is##TYPE()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) \
+ << #GETTER " didn't return type " #TYPE; \
+ } \
+ } while (false)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+# define ASSERT_HAS_JSON(GETTER, TYPE) \
+ do { \
+ ASSERT_FALSE((GETTER).isNull()); \
+ ASSERT_TRUE((GETTER).is##TYPE()); \
+ } while (false)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+// If yes, store the reference to Json::Value into VARIABLE.
+# define GET_JSON(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ const Json::Value& VARIABLE = (GETTER)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+// If yes, store the value as `const TYPE` into VARIABLE.
+# define GET_JSON_VALUE(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ const auto VARIABLE = (GETTER).as##TYPE()
+
+// Non-const GET_JSON_VALUE.
+# define GET_JSON_MUTABLE_VALUE(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ auto VARIABLE = (GETTER).as##TYPE()
+
+// Checks that the GETTER's value is present, is of the expected TYPE, and has
+// the expected VALUE. (Non-critical)
+# define EXPECT_EQ_JSON(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).is##TYPE()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) \
+ << #GETTER " didn't return type " #TYPE; \
+ } else { \
+ EXPECT_EQ((GETTER).as##TYPE(), (VALUE)); \
+ } \
+ } while (false)
+
+// Checks that the GETTER's value is present, and is a valid index into the
+// STRINGTABLE array, pointing at the expected STRING.
+# define EXPECT_EQ_STRINGTABLE(GETTER, STRINGTABLE, STRING) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isUInt()) { \
+ EXPECT_TRUE((GETTER).isUInt()) << #GETTER " didn't return an index"; \
+ } else { \
+ EXPECT_LT((GETTER).asUInt(), (STRINGTABLE).size()); \
+ EXPECT_EQ_JSON((STRINGTABLE)[(GETTER).asUInt()], String, (STRING)); \
+ } \
+ } while (false)
+
+# define EXPECT_JSON_ARRAY_CONTAINS(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isArray()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
+ } else if (const Json::ArrayIndex size = (GETTER).size(); size == 0u) { \
+ EXPECT_NE(size, 0u) << #GETTER " is an empty array"; \
+ } else { \
+ bool found = false; \
+ for (Json::ArrayIndex i = 0; i < size; ++i) { \
+ if (!(GETTER)[i].is##TYPE()) { \
+ EXPECT_TRUE((GETTER)[i].is##TYPE()) \
+ << #GETTER "[" << i << "] is not " #TYPE; \
+ break; \
+ } \
+ if ((GETTER)[i].as##TYPE() == (VALUE)) { \
+ found = true; \
+ break; \
+ } \
+ } \
+ EXPECT_TRUE(found) << #GETTER " doesn't contain " #VALUE; \
+ } \
+ } while (false)
+
+# define EXPECT_JSON_ARRAY_EXCLUDES(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isArray()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
+ } else { \
+ const Json::ArrayIndex size = (GETTER).size(); \
+ for (Json::ArrayIndex i = 0; i < size; ++i) { \
+ if (!(GETTER)[i].is##TYPE()) { \
+ EXPECT_TRUE((GETTER)[i].is##TYPE()) \
+ << #GETTER "[" << i << "] is not " #TYPE; \
+ break; \
+ } \
+ if ((GETTER)[i].as##TYPE() == (VALUE)) { \
+ EXPECT_TRUE((GETTER)[i].as##TYPE() != (VALUE)) \
+ << #GETTER " contains " #VALUE; \
+ break; \
+ } \
+ } \
+ } \
+ } while (false)
+
+// Check that the given process root contains all the expected properties.
+static void JSONRootCheck(const Json::Value& aRoot,
+ bool aWithMainThread = true) {
+ ASSERT_TRUE(aRoot.isObject());
+
+ EXPECT_HAS_JSON(aRoot["libs"], Array);
+
+ GET_JSON(meta, aRoot["meta"], Object);
+ EXPECT_HAS_JSON(meta["version"], UInt);
+ EXPECT_HAS_JSON(meta["startTime"], Double);
+ EXPECT_HAS_JSON(meta["profilingStartTime"], Double);
+ EXPECT_HAS_JSON(meta["contentEarliestTime"], Double);
+ EXPECT_HAS_JSON(meta["profilingEndTime"], Double);
+
+ EXPECT_HAS_JSON(aRoot["pages"], Array);
+
+ EXPECT_HAS_JSON(aRoot["profilerOverhead"], Object);
+
+ // "counters" is only present if there is any data to report.
+ // Test that expect "counters" should test for its presence first.
+ if (aRoot.isMember("counters")) {
+ // We have "counters", test their overall validity.
+ GET_JSON(counters, aRoot["counters"], Array);
+ for (const Json::Value& counter : counters) {
+ ASSERT_TRUE(counter.isObject());
+ EXPECT_HAS_JSON(counter["name"], String);
+ EXPECT_HAS_JSON(counter["category"], String);
+ EXPECT_HAS_JSON(counter["description"], String);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_HAS_JSON(sampleGroup["id"], UInt);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesTime, samplesSchema["time"], UInt);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ double previousTime = 0.0;
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ GET_JSON_VALUE(time, sample[samplesTime], Double);
+ EXPECT_GE(time, previousTime);
+ previousTime = time;
+ if (sample.isValidIndex(samplesNumber)) {
+ EXPECT_HAS_JSON(sample[samplesNumber], UInt64);
+ }
+ if (sample.isValidIndex(samplesCount)) {
+ EXPECT_HAS_JSON(sample[samplesCount], Int64);
+ }
+ }
+ }
+ }
+ }
+
+ GET_JSON(threads, aRoot["threads"], Array);
+ const Json::ArrayIndex threadCount = threads.size();
+ for (Json::ArrayIndex i = 0; i < threadCount; ++i) {
+ GET_JSON(thread, threads[i], Object);
+ EXPECT_HAS_JSON(thread["processType"], String);
+ EXPECT_HAS_JSON(thread["name"], String);
+ EXPECT_HAS_JSON(thread["registerTime"], Double);
+ GET_JSON(samples, thread["samples"], Object);
+ EXPECT_HAS_JSON(thread["markers"], Object);
+ EXPECT_HAS_JSON(thread["pid"], Int64);
+ EXPECT_HAS_JSON(thread["tid"], Int64);
+ GET_JSON(stackTable, thread["stackTable"], Object);
+ GET_JSON(frameTable, thread["frameTable"], Object);
+ GET_JSON(stringTable, thread["stringTable"], Array);
+
+ GET_JSON(stackTableSchema, stackTable["schema"], Object);
+ EXPECT_GE(stackTableSchema.size(), 2u);
+ GET_JSON_VALUE(stackTablePrefix, stackTableSchema["prefix"], UInt);
+ GET_JSON_VALUE(stackTableFrame, stackTableSchema["frame"], UInt);
+ GET_JSON(stackTableData, stackTable["data"], Array);
+
+ GET_JSON(frameTableSchema, frameTable["schema"], Object);
+ EXPECT_GE(frameTableSchema.size(), 1u);
+ GET_JSON_VALUE(frameTableLocation, frameTableSchema["location"], UInt);
+ GET_JSON(frameTableData, frameTable["data"], Array);
+
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ GET_JSON_VALUE(sampleStackIndex, samplesSchema["stack"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(sampleStackIndex)) {
+ if (!sample[sampleStackIndex].isNull()) {
+ GET_JSON_MUTABLE_VALUE(stack, sample[sampleStackIndex], UInt);
+ EXPECT_TRUE(stackTableData.isValidIndex(stack));
+ for (;;) {
+ // `stack` (from the sample, or from the callee frame's "prefix" in
+ // the previous loop) points into the stackTable.
+ GET_JSON(stackTableEntry, stackTableData[stack], Array);
+ GET_JSON_VALUE(frame, stackTableEntry[stackTableFrame], UInt);
+
+ // The stackTable entry's "frame" points into the frameTable.
+ EXPECT_TRUE(frameTableData.isValidIndex(frame));
+ GET_JSON(frameTableEntry, frameTableData[frame], Array);
+ GET_JSON_VALUE(location, frameTableEntry[frameTableLocation], UInt);
+
+ // The frameTable entry's "location" points at a string.
+ EXPECT_TRUE(stringTable.isValidIndex(location));
+
+ // The stackTable entry's "prefix" is null for the root frame.
+ if (stackTableEntry[stackTablePrefix].isNull()) {
+ break;
+ }
+ // Otherwise it recursively points at the caller in the stackTable.
+ GET_JSON_VALUE(prefix, stackTableEntry[stackTablePrefix], UInt);
+ EXPECT_TRUE(stackTableData.isValidIndex(prefix));
+ stack = prefix;
+ }
+ }
+ }
+ }
+ }
+
+ if (aWithMainThread) {
+ ASSERT_GT(threadCount, 0u);
+ GET_JSON(thread0, threads[0], Object);
+ EXPECT_EQ_JSON(thread0["name"], String, "GeckoMain");
+ }
+
+ EXPECT_HAS_JSON(aRoot["pausedRanges"], Array);
+
+ const Json::Value& processes = aRoot["processes"];
+ if (!processes.isNull()) {
+ ASSERT_TRUE(processes.isArray());
+ const Json::ArrayIndex processCount = processes.size();
+ for (Json::ArrayIndex i = 0; i < processCount; ++i) {
+ GET_JSON(process, processes[i], Object);
+ JSONRootCheck(process, aWithMainThread);
+ }
+ }
+
+ GET_JSON(profilingLog, aRoot["profilingLog"], Object);
+ EXPECT_EQ(profilingLog.size(), 1u);
+ for (auto it = profilingLog.begin(); it != profilingLog.end(); ++it) {
+ // The key should be a pid.
+ const auto key = it.name();
+ for (const auto letter : key) {
+ EXPECT_GE(letter, '0');
+ EXPECT_LE(letter, '9');
+ }
+ // And the value should be an object.
+ GET_JSON(logForPid, profilingLog[key], Object);
+ // Its content is not defined, but we expect at least these:
+ EXPECT_HAS_JSON(logForPid["profilingLogBegin_TSms"], Double);
+ EXPECT_HAS_JSON(logForPid["profilingLogEnd_TSms"], Double);
+ }
+}
+
+// Check that various expected top properties are in the JSON, and then call the
+// provided `aJSONCheckFunction` with the JSON root object.
+template <typename JSONCheckFunction>
+void JSONOutputCheck(const char* aOutput,
+ JSONCheckFunction&& aJSONCheckFunction) {
+ ASSERT_NE(aOutput, nullptr);
+
+ JSONWhitespaceCheck(aOutput);
+
+ // Extract JSON.
+ Json::Value parsedRoot;
+ Json::CharReaderBuilder builder;
+ const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
+ ASSERT_TRUE(
+ reader->parse(aOutput, strchr(aOutput, '\0'), &parsedRoot, nullptr));
+
+ JSONRootCheck(parsedRoot);
+
+ std::forward<JSONCheckFunction>(aJSONCheckFunction)(parsedRoot);
+}
+
+// Returns `static_cast<SamplingState>(-1)` if callback could not be installed.
+static SamplingState WaitForSamplingState() {
+ Atomic<int> samplingState{-1};
+
+ if (!profiler_callback_after_sampling([&](SamplingState aSamplingState) {
+ samplingState = static_cast<int>(aSamplingState);
+ })) {
+ return static_cast<SamplingState>(-1);
+ }
+
+ while (samplingState == -1) {
+ }
+
+ return static_cast<SamplingState>(static_cast<int>(samplingState));
+}
+
+typedef Vector<const char*> StrVec;
+
+static void InactiveFeaturesAndParamsCheck() {
+ int entries;
+ Maybe<double> duration;
+ double interval;
+ uint32_t features;
+ StrVec filters;
+ uint64_t activeTabID;
+
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::NativeAllocations));
+
+ profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
+ &activeTabID);
+
+ ASSERT_TRUE(entries == 0);
+ ASSERT_TRUE(duration == Nothing());
+ ASSERT_TRUE(interval == 0);
+ ASSERT_TRUE(features == 0);
+ ASSERT_TRUE(filters.empty());
+ ASSERT_TRUE(activeTabID == 0);
+}
+
+static void ActiveParamsCheck(int aEntries, double aInterval,
+ uint32_t aFeatures, const char** aFilters,
+ size_t aFiltersLen, uint64_t aActiveTabID,
+ const Maybe<double>& aDuration = Nothing()) {
+ int entries;
+ Maybe<double> duration;
+ double interval;
+ uint32_t features;
+ StrVec filters;
+ uint64_t activeTabID;
+
+ profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
+ &activeTabID);
+
+ ASSERT_TRUE(entries == aEntries);
+ ASSERT_TRUE(duration == aDuration);
+ ASSERT_TRUE(interval == aInterval);
+ ASSERT_TRUE(features == aFeatures);
+ ASSERT_TRUE(filters.length() == aFiltersLen);
+ ASSERT_TRUE(activeTabID == aActiveTabID);
+ for (size_t i = 0; i < aFiltersLen; i++) {
+ ASSERT_TRUE(strcmp(filters[i], aFilters[i]) == 0);
+ }
+}
+
+TEST(GeckoProfiler, FeaturesAndParams)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ // Try a couple of features and filters.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+# define PROFILER_DEFAULT_DURATION 20 /* seconds, for tests only */
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 100,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 100,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try some different features and filters.
+ {
+ uint32_t features =
+ ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages;
+ const char* filters[] = {"GeckoMain", "Foo", "Bar"};
+
+ // Testing with some arbitrary buffer size (as could be provided by
+ // external code), which we convert to the appropriate power of 2.
+ profiler_start(PowerOfTwo32(999999), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 123, Some(25.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(int(PowerOfTwo32(999999).Value()), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 123, Some(25.0));
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try with no duration
+ {
+ uint32_t features =
+ ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages;
+ const char* filters[] = {"GeckoMain", "Foo", "Bar"};
+
+ profiler_start(PowerOfTwo32(999999), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(int(PowerOfTwo32(999999).Value()), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try all supported features, and filters that match all threads.
+ {
+ uint32_t availableFeatures = profiler_get_available_features();
+ const char* filters[] = {""};
+
+ profiler_start(PowerOfTwo32(88888), 10, availableFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Some(15.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PowerOfTwo32(88888).Value(), 10, availableFeatures,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(15.0));
+
+ // Don't call profiler_stop() here.
+ }
+
+ // Try no features, and filters that match no threads.
+ {
+ uint32_t features = 0;
+ const char* filters[] = {"NoThreadWillMatchThis"};
+
+ // Second profiler_start() call in a row without an intervening
+ // profiler_stop(); this will do an implicit profiler_stop() and restart.
+ profiler_start(PowerOfTwo32(0), 0, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Some(0.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ // Entries and intervals go to defaults if 0 is specified.
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+
+ // These calls are no-ops.
+ profiler_stop();
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+}
+
+TEST(GeckoProfiler, EnsureStarted)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+ {
+ // Inactive -> Active
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ActiveParamsCheck(
+ PROFILER_DEFAULT_ENTRIES.Value(), PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(PROFILER_DEFAULT_DURATION));
+ }
+
+ {
+ // Active -> Active with same settings
+
+ Maybe<ProfilerBufferInfo> info0 = profiler_get_buffer_info();
+ ASSERT_TRUE(info0->mRangeEnd > 0);
+
+ // First, write some samples into the buffer.
+ PR_Sleep(PR_MillisecondsToInterval(500));
+
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd > info0->mRangeEnd);
+
+ // Call profiler_ensure_started with the same settings as before.
+ // This operation must not clear our buffer!
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ActiveParamsCheck(
+ PROFILER_DEFAULT_ENTRIES.Value(), PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(PROFILER_DEFAULT_DURATION));
+
+ // Check that our position in the buffer stayed the same or advanced, but
+ // not by much, and the range-start after profiler_ensure_started shouldn't
+ // have passed the range-end before.
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeEnd >= info1->mRangeEnd);
+ ASSERT_TRUE(info2->mRangeEnd - info1->mRangeEnd <
+ info1->mRangeEnd - info0->mRangeEnd);
+ ASSERT_TRUE(info2->mRangeStart < info1->mRangeEnd);
+ }
+
+ {
+ // Active -> Active with *different* settings
+
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+
+ // Call profiler_ensure_started with a different feature set than the one
+ // it's currently running with. This is supposed to stop and restart the
+ // profiler, thereby discarding the buffer contents.
+ uint32_t differentFeatures = features | ProfilerFeature::CPUUtilization;
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ differentFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, differentFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Check the the buffer was cleared, so its range-start should be at/after
+ // its range-end before.
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeStart >= info1->mRangeEnd);
+ }
+
+ {
+ // Active -> Inactive
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+}
+
+TEST(GeckoProfiler, MultiRegistration)
+{
+ // This whole test only checks that function calls don't crash, they don't
+ // actually verify that threads get profiled or not.
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread, no unreg", &top);
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() { profiler_unregister_thread(); });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread 1st", &top);
+ profiler_unregister_thread();
+ profiler_register_thread("thread 2nd", &top);
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread once", &top);
+ profiler_register_thread("thread again", &top);
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread to unreg twice", &top);
+ profiler_unregister_thread();
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+}
+
+TEST(GeckoProfiler, DifferentThreads)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ // Control the profiler on a background thread and verify flags on the
+ // main thread.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ thread->Dispatch(
+ NS_NewRunnableFunction("GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() {
+ profiler_start(PROFILER_DEFAULT_ENTRIES,
+ PROFILER_DEFAULT_INTERVAL,
+ features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+ }),
+ NS_DISPATCH_SYNC);
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ thread->Dispatch(
+ NS_NewRunnableFunction("GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() { profiler_stop(); }),
+ NS_DISPATCH_SYNC);
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Control the profiler on the main thread and verify flags on a
+ // background thread.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ thread->Dispatch(
+ NS_NewRunnableFunction(
+ "GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() {
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(
+ !profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(
+ !profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+ }),
+ NS_DISPATCH_SYNC);
+
+ profiler_stop();
+
+ thread->Dispatch(
+ NS_NewRunnableFunction("GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() { InactiveFeaturesAndParamsCheck(); }),
+ NS_DISPATCH_SYNC);
+ }
+
+ thread->Shutdown();
+}
+
+TEST(GeckoProfiler, GetBacktrace)
+{
+ ASSERT_TRUE(!profiler_get_backtrace());
+
+ {
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // These will be destroyed while the profiler is active.
+ static const int N = 100;
+ {
+ UniqueProfilerBacktrace u[N];
+ for (int i = 0; i < N; i++) {
+ u[i] = profiler_get_backtrace();
+ ASSERT_TRUE(u[i]);
+ }
+ }
+
+ // These will be destroyed after the profiler stops.
+ UniqueProfilerBacktrace u[N];
+ for (int i = 0; i < N; i++) {
+ u[i] = profiler_get_backtrace();
+ ASSERT_TRUE(u[i]);
+ }
+
+ profiler_stop();
+ }
+
+ ASSERT_TRUE(!profiler_get_backtrace());
+}
+
+TEST(GeckoProfiler, Pause)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test must run on the main thread";
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain", "Profiled GeckoProfiler.Pause"};
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - before start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - before start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ }}.join();
+
+ // Check that we are writing samples while not paused.
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+ PR_Sleep(PR_MillisecondsToInterval(500));
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd != info2->mRangeEnd);
+
+ // Check that we are writing markers while not paused.
+ ASSERT_TRUE(profiler_thread_is_being_profiled_for_markers());
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled_for_markers(ProfilerThreadId{}));
+ ASSERT_TRUE(profiler_thread_is_being_profiled_for_markers(
+ profiler_current_thread_id()));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled_for_markers(profiler_main_thread_id()));
+ info1 = profiler_get_buffer_info();
+ PROFILER_MARKER_UNTYPED("Not paused", OTHER, {});
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd != info2->mRangeEnd);
+
+ profiler_pause();
+
+ ASSERT_TRUE(profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers());
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled_for_markers(ProfilerThreadId{}));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers(
+ profiler_current_thread_id()));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers(
+ profiler_main_thread_id()));
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after pause");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after pause");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+
+ // Check that we are not writing samples while paused.
+ info1 = profiler_get_buffer_info();
+ PR_Sleep(PR_MillisecondsToInterval(500));
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd == info2->mRangeEnd);
+
+ // Check that we are now writing markers while paused.
+ info1 = profiler_get_buffer_info();
+ PROFILER_MARKER_UNTYPED("Paused", OTHER, {});
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd == info2->mRangeEnd);
+ PROFILER_MARKER_UNTYPED("Paused v2", OTHER, {});
+ Maybe<ProfilerBufferInfo> info3 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeEnd == info3->mRangeEnd);
+
+ profiler_resume();
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after resume");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after resume");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ }}.join();
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD("Ignored GeckoProfiler.Pause - after stop");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after stop");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+}
+
+TEST(GeckoProfiler, Markers)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ PROFILER_MARKER("tracing event", OTHER, {}, Tracing, "A");
+ PROFILER_MARKER("tracing start", OTHER, MarkerTiming::IntervalStart(),
+ Tracing, "A");
+ PROFILER_MARKER("tracing end", OTHER, MarkerTiming::IntervalEnd(), Tracing,
+ "A");
+
+ auto bt = profiler_capture_backtrace();
+ PROFILER_MARKER("tracing event with stack", OTHER,
+ MarkerStack::TakeBacktrace(std::move(bt)), Tracing, "B");
+
+ { AUTO_PROFILER_TRACING_MARKER("C", "auto tracing", OTHER); }
+
+ PROFILER_MARKER_UNTYPED("M1", OTHER, {});
+ PROFILER_MARKER_UNTYPED("M3", OTHER, {});
+
+ // Create three strings: two that are the maximum allowed length, and one that
+ // is one char longer.
+ static const size_t kMax = ProfileBuffer::kMaxFrameKeyLength;
+ UniquePtr<char[]> okstr1 = MakeUnique<char[]>(kMax);
+ UniquePtr<char[]> okstr2 = MakeUnique<char[]>(kMax);
+ UniquePtr<char[]> longstr = MakeUnique<char[]>(kMax + 1);
+ UniquePtr<char[]> longstrCut = MakeUnique<char[]>(kMax + 1);
+ for (size_t i = 0; i < kMax; i++) {
+ okstr1[i] = 'a';
+ okstr2[i] = 'b';
+ longstr[i] = 'c';
+ longstrCut[i] = 'c';
+ }
+ okstr1[kMax - 1] = '\0';
+ okstr2[kMax - 1] = '\0';
+ longstr[kMax] = '\0';
+ longstrCut[kMax] = '\0';
+ // Should be output as-is.
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, "");
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, okstr1.get());
+ // Should be output as label + space + okstr2.
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("okstr2", LAYOUT, okstr2.get());
+ // Should be output with kMax length, ending with "...\0".
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, longstr.get());
+ ASSERT_EQ(longstrCut[kMax - 4], 'c');
+ longstrCut[kMax - 4] = '.';
+ ASSERT_EQ(longstrCut[kMax - 3], 'c');
+ longstrCut[kMax - 3] = '.';
+ ASSERT_EQ(longstrCut[kMax - 2], 'c');
+ longstrCut[kMax - 2] = '.';
+ ASSERT_EQ(longstrCut[kMax - 1], 'c');
+ longstrCut[kMax - 1] = '\0';
+
+ // Test basic markers 2.0.
+ EXPECT_TRUE(
+ profiler_add_marker("default-templated markers 2.0 with empty options",
+ geckoprofiler::category::OTHER, {}));
+
+ PROFILER_MARKER_UNTYPED(
+ "default-templated markers 2.0 with option", OTHER,
+ MarkerStack::TakeBacktrace(profiler_capture_backtrace()));
+
+ PROFILER_MARKER("explicitly-default-templated markers 2.0 with empty options",
+ OTHER, {}, NoPayload);
+
+ EXPECT_TRUE(profiler_add_marker(
+ "explicitly-default-templated markers 2.0 with option",
+ geckoprofiler::category::OTHER, {},
+ ::geckoprofiler::markers::NoPayload{}));
+
+ // Used in markers below.
+ TimeStamp ts1 = TimeStamp::Now();
+
+ // Sleep briefly to ensure a sample is taken and the pending markers are
+ // processed.
+ PR_Sleep(PR_MillisecondsToInterval(500));
+
+ // Used in markers below.
+ TimeStamp ts2 = TimeStamp::Now();
+ // ts1 and ts2 should be different thanks to the sleep.
+ EXPECT_NE(ts1, ts2);
+
+ // Test most marker payloads.
+
+ // Keep this one first! (It's used to record `ts1` and `ts2`, to compare
+ // to serialized numbers in other markers.)
+ EXPECT_TRUE(profiler_add_marker("FirstMarker", geckoprofiler::category::OTHER,
+ MarkerTiming::Interval(ts1, ts2),
+ geckoprofiler::markers::TextMarker{},
+ "First Marker"));
+
+ // User-defined marker type with different properties, and fake schema.
+ struct GtestMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter, int aInt,
+ double aDouble, const mozilla::ProfilerString8View& aText,
+ const mozilla::ProfilerString8View& aUniqueText,
+ const mozilla::TimeStamp& aTime) {
+ aWriter.NullProperty("null");
+ aWriter.BoolProperty("bool-false", false);
+ aWriter.BoolProperty("bool-true", true);
+ aWriter.IntProperty("int", aInt);
+ aWriter.DoubleProperty("double", aDouble);
+ aWriter.StringProperty("text", aText);
+ aWriter.UniqueStringProperty("unique text", aUniqueText);
+ aWriter.UniqueStringProperty("unique text again", aUniqueText);
+ aWriter.TimeProperty("time", aTime);
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ // Note: This is an test function that is not intended to actually output
+ // that correctly matches StreamJSONMarkerData data above! Instead we only
+ // test that it outputs the expected JSON at the end.
+ using MS = mozilla::MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
+ MS::Location::TimelineOverview, MS::Location::TimelineMemory,
+ MS::Location::TimelineIPC, MS::Location::TimelineFileIO,
+ MS::Location::StackChart};
+ // All label functions.
+ schema.SetChartLabel("chart label");
+ schema.SetTooltipLabel("tooltip label");
+ schema.SetTableLabel("table label");
+ // All data functions, all formats, all "searchable" values.
+ schema.AddKeyFormat("key with url", MS::Format::Url);
+ schema.AddKeyLabelFormat("key with label filePath", "label filePath",
+ MS::Format::FilePath);
+ schema.AddKeyFormatSearchable("key with string not-searchable",
+ MS::Format::String,
+ MS::Searchable::NotSearchable);
+ schema.AddKeyLabelFormatSearchable("key with label duration searchable",
+ "label duration", MS::Format::Duration,
+ MS::Searchable::Searchable);
+ schema.AddKeyFormat("key with time", MS::Format::Time);
+ schema.AddKeyFormat("key with seconds", MS::Format::Seconds);
+ schema.AddKeyFormat("key with milliseconds", MS::Format::Milliseconds);
+ schema.AddKeyFormat("key with microseconds", MS::Format::Microseconds);
+ schema.AddKeyFormat("key with nanoseconds", MS::Format::Nanoseconds);
+ schema.AddKeyFormat("key with bytes", MS::Format::Bytes);
+ schema.AddKeyFormat("key with percentage", MS::Format::Percentage);
+ schema.AddKeyFormat("key with integer", MS::Format::Integer);
+ schema.AddKeyFormat("key with decimal", MS::Format::Decimal);
+ schema.AddStaticLabelValue("static label", "static value");
+ return schema;
+ }
+ };
+ EXPECT_TRUE(
+ profiler_add_marker("Gtest custom marker", geckoprofiler::category::OTHER,
+ MarkerTiming::Interval(ts1, ts2), GtestMarker{}, 42,
+ 43.0, "gtest text", "gtest unique text", ts1));
+
+ // User-defined marker type with no data, special frontend schema.
+ struct GtestSpecialMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-special");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+ EXPECT_TRUE(profiler_add_marker("Gtest special marker",
+ geckoprofiler::category::OTHER, {},
+ GtestSpecialMarker{}));
+
+ // User-defined marker type that is never used, so it shouldn't appear in the
+ // output.
+ struct GtestUnusedMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-unused");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+
+ // Make sure the compiler doesn't complain about this unused struct.
+ mozilla::Unused << GtestUnusedMarker{};
+
+ // Other markers in alphabetical order of payload class names.
+
+ nsCOMPtr<nsIURI> uri;
+ ASSERT_TRUE(
+ NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), "http://mozilla.org/"_ns)));
+ // The marker name will be "Load <aChannelId>: <aURI>".
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 1,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_START,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheHit,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ /* nsIURI* aRedirectURI = nullptr */
+ /* uint64_t aRedirectChannelId = 0 */
+ );
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 2,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_STOP,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ Some(nsDependentCString("text/html")),
+ /* nsIURI* aRedirectURI = nullptr */ nullptr,
+ /* uint64_t aRedirectChannelId = 0 */ 0);
+
+ nsCOMPtr<nsIURI> redirectURI;
+ ASSERT_TRUE(NS_SUCCEEDED(
+ NS_NewURI(getter_AddRefs(redirectURI), "http://example.com/"_ns)));
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 3,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */
+ nsIChannelEventSink::REDIRECT_TEMPORARY,
+ /* uint64_t aRedirectChannelId = 0 */ 103);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 4,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */
+ nsIChannelEventSink::REDIRECT_PERMANENT,
+ /* uint64_t aRedirectChannelId = 0 */ 104);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 5,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL,
+ /* uint64_t aRedirectChannelId = 0 */ 105);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 6,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL |
+ nsIChannelEventSink::REDIRECT_STS_UPGRADE,
+ /* uint64_t aRedirectChannelId = 0 */ 106);
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 7,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_START,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ true
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ /* nsIURI* aRedirectURI = nullptr */
+ /* uint64_t aRedirectChannelId = 0 */
+ );
+
+ EXPECT_TRUE(profiler_add_marker(
+ "Text in main thread with stack", geckoprofiler::category::OTHER,
+ {MarkerStack::Capture(), MarkerTiming::Interval(ts1, ts2)},
+ geckoprofiler::markers::TextMarker{}, ""));
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from main thread with stack", geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+
+ std::thread registeredThread([]() {
+ AUTO_PROFILER_REGISTER_THREAD("Marker test sub-thread");
+ // Marker in non-profiled thread won't be stored.
+ EXPECT_FALSE(profiler_add_marker(
+ "Text in registered thread with stack", geckoprofiler::category::OTHER,
+ MarkerStack::Capture(), geckoprofiler::markers::TextMarker{}, ""));
+ // Marker will be stored in main thread, with stack from registered thread.
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from registered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+ });
+ registeredThread.join();
+
+ std::thread unregisteredThread([]() {
+ // Marker in unregistered thread won't be stored.
+ EXPECT_FALSE(profiler_add_marker("Text in unregistered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerStack::Capture(),
+ geckoprofiler::markers::TextMarker{}, ""));
+ // Marker will be stored in main thread, but stack cannot be captured in an
+ // unregistered thread.
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from unregistered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+ });
+ unregisteredThread.join();
+
+ EXPECT_TRUE(profiler_add_marker("Tracing", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::Tracing{},
+ "category"));
+
+ EXPECT_TRUE(profiler_add_marker("Text", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::TextMarker{},
+ "Text text"));
+
+ EXPECT_TRUE(profiler_add_marker(
+ "MediaSample", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::MediaSampleMarker{}, 123, 456, 789));
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ w.Start();
+ EXPECT_TRUE(::profiler_stream_json_for_this_process(w));
+ w.End();
+
+ EXPECT_FALSE(w.Failed());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+ ASSERT_TRUE(!!profile.get());
+
+ // Expected markers, in order.
+ enum State {
+ S_tracing_event,
+ S_tracing_start,
+ S_tracing_end,
+ S_tracing_event_with_stack,
+ S_tracing_auto_tracing_start,
+ S_tracing_auto_tracing_end,
+ S_M1,
+ S_M3,
+ S_Markers2DefaultEmptyOptions,
+ S_Markers2DefaultWithOptions,
+ S_Markers2ExplicitDefaultEmptyOptions,
+ S_Markers2ExplicitDefaultWithOptions,
+ S_FirstMarker,
+ S_CustomMarker,
+ S_SpecialMarker,
+ S_NetworkMarkerPayload_start,
+ S_NetworkMarkerPayload_stop,
+ S_NetworkMarkerPayload_redirect_temporary,
+ S_NetworkMarkerPayload_redirect_permanent,
+ S_NetworkMarkerPayload_redirect_internal,
+ S_NetworkMarkerPayload_redirect_internal_sts,
+ S_NetworkMarkerPayload_private_browsing,
+
+ S_TextWithStack,
+ S_TextToMTWithStack,
+ S_RegThread_TextToMTWithStack,
+ S_UnregThread_TextToMTWithStack,
+
+ S_LAST,
+ } state = State(0);
+
+ // These will be set when first read from S_FirstMarker, then
+ // compared in following markers.
+ // TODO: Compute these values from the timestamps.
+ double ts1Double = 0.0;
+ double ts2Double = 0.0;
+
+ JSONOutputCheck(profile.get(), [&](const Json::Value& root) {
+ {
+ GET_JSON(threads, root["threads"], Array);
+ ASSERT_EQ(threads.size(), 1u);
+
+ {
+ GET_JSON(thread0, threads[0], Object);
+
+ // Keep a reference to the string table in this block, it will be used
+ // below.
+ GET_JSON(stringTable, thread0["stringTable"], Array);
+ ASSERT_TRUE(stringTable.isArray());
+
+ // Test the expected labels in the string table.
+ bool foundEmpty = false;
+ bool foundOkstr1 = false;
+ bool foundOkstr2 = false;
+ const std::string okstr2Label = std::string("okstr2 ") + okstr2.get();
+ bool foundTooLong = false;
+ for (const auto& s : stringTable) {
+ ASSERT_TRUE(s.isString());
+ std::string sString = s.asString();
+ if (sString.empty()) {
+ EXPECT_FALSE(foundEmpty);
+ foundEmpty = true;
+ } else if (sString == okstr1.get()) {
+ EXPECT_FALSE(foundOkstr1);
+ foundOkstr1 = true;
+ } else if (sString == okstr2Label) {
+ EXPECT_FALSE(foundOkstr2);
+ foundOkstr2 = true;
+ } else if (sString == longstrCut.get()) {
+ EXPECT_FALSE(foundTooLong);
+ foundTooLong = true;
+ } else {
+ EXPECT_NE(sString, longstr.get());
+ }
+ }
+ EXPECT_TRUE(foundEmpty);
+ EXPECT_TRUE(foundOkstr1);
+ EXPECT_TRUE(foundOkstr2);
+ EXPECT_TRUE(foundTooLong);
+
+ {
+ GET_JSON(markers, thread0["markers"], Object);
+
+ {
+ GET_JSON(data, markers["data"], Array);
+
+ for (const Json::Value& marker : data) {
+ // Name the indexes into the marker tuple:
+ // [name, startTime, endTime, phase, category, payload]
+ const unsigned int NAME = 0u;
+ const unsigned int START_TIME = 1u;
+ const unsigned int END_TIME = 2u;
+ const unsigned int PHASE = 3u;
+ const unsigned int CATEGORY = 4u;
+ const unsigned int PAYLOAD = 5u;
+
+ const unsigned int PHASE_INSTANT = 0;
+ const unsigned int PHASE_INTERVAL = 1;
+ const unsigned int PHASE_START = 2;
+ const unsigned int PHASE_END = 3;
+
+ const unsigned int SIZE_WITHOUT_PAYLOAD = 5u;
+ const unsigned int SIZE_WITH_PAYLOAD = 6u;
+
+ ASSERT_TRUE(marker.isArray());
+ // The payload is optional.
+ ASSERT_GE(marker.size(), SIZE_WITHOUT_PAYLOAD);
+ ASSERT_LE(marker.size(), SIZE_WITH_PAYLOAD);
+
+ // root.threads[0].markers.data[i] is an array with 5 or 6
+ // elements.
+
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ std::string nameString = name.asString();
+
+ EXPECT_TRUE(marker[START_TIME].isNumeric());
+ EXPECT_TRUE(marker[END_TIME].isNumeric());
+ EXPECT_TRUE(marker[PHASE].isUInt());
+ EXPECT_TRUE(marker[PHASE].asUInt() < 4);
+ EXPECT_TRUE(marker[CATEGORY].isUInt());
+
+# define EXPECT_TIMING_INSTANT \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INSTANT);
+# define EXPECT_TIMING_INTERVAL \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_NE(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INTERVAL);
+# define EXPECT_TIMING_START \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_START);
+# define EXPECT_TIMING_END \
+ EXPECT_EQ(marker[START_TIME].asDouble(), 0); \
+ EXPECT_NE(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_END);
+
+# define EXPECT_TIMING_INSTANT_AT(t) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), t); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INSTANT);
+# define EXPECT_TIMING_INTERVAL_AT(start, end) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), start); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), end); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INTERVAL);
+# define EXPECT_TIMING_START_AT(start) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), start); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_START);
+# define EXPECT_TIMING_END_AT(end) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), end); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_END);
+
+ if (marker.size() == SIZE_WITHOUT_PAYLOAD) {
+ // root.threads[0].markers.data[i] is an array with 5 elements,
+ // so there is no payload.
+ if (nameString == "M1") {
+ ASSERT_EQ(state, S_M1);
+ state = State(state + 1);
+ } else if (nameString == "M3") {
+ ASSERT_EQ(state, S_M3);
+ state = State(state + 1);
+ } else if (nameString ==
+ "default-templated markers 2.0 with empty options") {
+ EXPECT_EQ(state, S_Markers2DefaultEmptyOptions);
+ state = State(S_Markers2DefaultEmptyOptions + 1);
+// TODO: Re-enable this when bug 1646714 lands, and check for stack.
+# if 0
+ } else if (nameString ==
+ "default-templated markers 2.0 with option") {
+ EXPECT_EQ(state, S_Markers2DefaultWithOptions);
+ state = State(S_Markers2DefaultWithOptions + 1);
+# endif
+ } else if (nameString ==
+ "explicitly-default-templated markers 2.0 with "
+ "empty "
+ "options") {
+ EXPECT_EQ(state, S_Markers2ExplicitDefaultEmptyOptions);
+ state = State(S_Markers2ExplicitDefaultEmptyOptions + 1);
+ } else if (nameString ==
+ "explicitly-default-templated markers 2.0 with "
+ "option") {
+ EXPECT_EQ(state, S_Markers2ExplicitDefaultWithOptions);
+ state = State(S_Markers2ExplicitDefaultWithOptions + 1);
+ }
+ } else {
+ // root.threads[0].markers.data[i] is an array with 6 elements,
+ // so there is a payload.
+ GET_JSON(payload, marker[PAYLOAD], Object);
+
+ // root.threads[0].markers.data[i][PAYLOAD] is an object
+ // (payload).
+
+ // It should at least have a "type" string.
+ GET_JSON(type, payload["type"], String);
+ std::string typeString = type.asString();
+
+ if (nameString == "tracing event") {
+ EXPECT_EQ(state, S_tracing_event);
+ state = State(S_tracing_event + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_INSTANT;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing start") {
+ EXPECT_EQ(state, S_tracing_start);
+ state = State(S_tracing_start + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_START;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing end") {
+ EXPECT_EQ(state, S_tracing_end);
+ state = State(S_tracing_end + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_END;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing event with stack") {
+ EXPECT_EQ(state, S_tracing_event_with_stack);
+ state = State(S_tracing_event_with_stack + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_INSTANT;
+ EXPECT_EQ_JSON(payload["category"], String, "B");
+ EXPECT_TRUE(payload["stack"].isObject());
+
+ } else if (nameString == "auto tracing") {
+ switch (state) {
+ case S_tracing_auto_tracing_start:
+ state = State(S_tracing_auto_tracing_start + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_START;
+ EXPECT_EQ_JSON(payload["category"], String, "C");
+ EXPECT_TRUE(payload["stack"].isNull());
+ break;
+ case S_tracing_auto_tracing_end:
+ state = State(S_tracing_auto_tracing_end + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_END;
+ EXPECT_EQ_JSON(payload["category"], String, "C");
+ ASSERT_TRUE(payload["stack"].isNull());
+ break;
+ default:
+ EXPECT_TRUE(state == S_tracing_auto_tracing_start ||
+ state == S_tracing_auto_tracing_end);
+ break;
+ }
+
+ } else if (nameString ==
+ "default-templated markers 2.0 with option") {
+ // TODO: Remove this when bug 1646714 lands.
+ EXPECT_EQ(state, S_Markers2DefaultWithOptions);
+ state = State(S_Markers2DefaultWithOptions + 1);
+ EXPECT_EQ(typeString, "NoPayloadUserData");
+ EXPECT_FALSE(payload["stack"].isNull());
+
+ } else if (nameString == "FirstMarker") {
+ // Record start and end times, to compare with timestamps in
+ // following markers.
+ EXPECT_EQ(state, S_FirstMarker);
+ ts1Double = marker[START_TIME].asDouble();
+ ts2Double = marker[END_TIME].asDouble();
+ state = State(S_FirstMarker + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_EQ_JSON(payload["name"], String, "First Marker");
+
+ } else if (nameString == "Gtest custom marker") {
+ EXPECT_EQ(state, S_CustomMarker);
+ state = State(S_CustomMarker + 1);
+ EXPECT_EQ(typeString, "markers-gtest");
+ EXPECT_EQ(payload.size(), 1u + 9u);
+ EXPECT_TRUE(payload["null"].isNull());
+ EXPECT_EQ_JSON(payload["bool-false"], Bool, false);
+ EXPECT_EQ_JSON(payload["bool-true"], Bool, true);
+ EXPECT_EQ_JSON(payload["int"], Int64, 42);
+ EXPECT_EQ_JSON(payload["double"], Double, 43.0);
+ EXPECT_EQ_JSON(payload["text"], String, "gtest text");
+ // Unique strings can be fetched from the string table.
+ ASSERT_TRUE(payload["unique text"].isUInt());
+ auto textIndex = payload["unique text"].asUInt();
+ GET_JSON(uniqueText, stringTable[textIndex], String);
+ ASSERT_TRUE(uniqueText.isString());
+ ASSERT_EQ(uniqueText.asString(), "gtest unique text");
+ // The duplicate unique text should have the exact same index.
+ EXPECT_EQ_JSON(payload["unique text again"], UInt, textIndex);
+ EXPECT_EQ_JSON(payload["time"], Double, ts1Double);
+
+ } else if (nameString == "Gtest special marker") {
+ EXPECT_EQ(state, S_SpecialMarker);
+ state = State(S_SpecialMarker + 1);
+ EXPECT_EQ(typeString, "markers-gtest-special");
+ EXPECT_EQ(payload.size(), 1u) << "Only 'type' in the payload";
+
+ } else if (nameString == "Load 1: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_start);
+ state = State(S_NetworkMarkerPayload_start + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 1);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Hit");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 2: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_stop);
+ state = State(S_NetworkMarkerPayload_stop + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 2);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_EQ_JSON(payload["contentType"], String, "text/html");
+
+ } else if (nameString == "Load 3: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_temporary);
+ state = State(S_NetworkMarkerPayload_redirect_temporary + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 3);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Temporary");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 103);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 4: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_permanent);
+ state = State(S_NetworkMarkerPayload_redirect_permanent + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 4);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Permanent");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 104);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 5: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_internal);
+ state = State(S_NetworkMarkerPayload_redirect_internal + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 5);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 105);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 6: http://mozilla.org/") {
+ EXPECT_EQ(state,
+ S_NetworkMarkerPayload_redirect_internal_sts);
+ state =
+ State(S_NetworkMarkerPayload_redirect_internal_sts + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 6);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, true);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 106);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 7: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_private_browsing);
+ state = State(S_NetworkMarkerPayload_private_browsing + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 7);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_EQ_JSON(payload["isPrivateBrowsing"], Bool, true);
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_TRUE(payload["contentType"].isNull());
+ } else if (nameString == "Text in main thread with stack") {
+ EXPECT_EQ(state, S_TextWithStack);
+ state = State(S_TextWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_TIMING_INTERVAL_AT(ts1Double, ts2Double);
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString == "Text from main thread with stack") {
+ EXPECT_EQ(state, S_TextToMTWithStack);
+ state = State(S_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString ==
+ "Text in registered thread with stack") {
+ ADD_FAILURE()
+ << "Unexpected 'Text in registered thread with stack'";
+
+ } else if (nameString ==
+ "Text from registered thread with stack") {
+ EXPECT_EQ(state, S_RegThread_TextToMTWithStack);
+ state = State(S_RegThread_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString ==
+ "Text in unregistered thread with stack") {
+ ADD_FAILURE()
+ << "Unexpected 'Text in unregistered thread with stack'";
+
+ } else if (nameString ==
+ "Text from unregistered thread with stack") {
+ EXPECT_EQ(state, S_UnregThread_TextToMTWithStack);
+ state = State(S_UnregThread_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_TRUE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+ }
+ } // marker with payload
+ } // for (marker : data)
+ } // markers.data
+ } // markers
+ } // thread0
+ } // threads
+ // We should have read all expected markers.
+ EXPECT_EQ(state, S_LAST);
+
+ {
+ GET_JSON(meta, root["meta"], Object);
+
+ {
+ GET_JSON(markerSchema, meta["markerSchema"], Array);
+
+ std::set<std::string> testedSchemaNames;
+
+ for (const Json::Value& schema : markerSchema) {
+ GET_JSON(name, schema["name"], String);
+ const std::string nameString = name.asString();
+
+ GET_JSON(display, schema["display"], Array);
+
+ GET_JSON(data, schema["data"], Array);
+
+ EXPECT_TRUE(
+ testedSchemaNames
+ .insert(std::string(nameString.data(), nameString.size()))
+ .second)
+ << "Each schema name should be unique (inserted once in the set)";
+
+ if (nameString == "Text") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "name");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Details");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "NoPayloadUserData") {
+ // TODO: Remove this when bug 1646714 lands.
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "FileIO") {
+ // These are defined in ProfilerIOInterposeObserver.cpp
+
+ } else if (nameString == "tracing") {
+ EXPECT_EQ(display.size(), 3u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+ EXPECT_EQ(display[2u].asString(), "timeline-overview");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "category");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "BHR-detected hang") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "MainThreadLongTask") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "category");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "Log") {
+ EXPECT_EQ(display.size(), 1u);
+ EXPECT_EQ(display[0u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 2u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "module");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Module");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "name");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Name");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "string");
+
+ } else if (nameString == "MediaSample") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 3u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "sampleStartTimeUs");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Sample start time");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "sampleEndTimeUs");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Sample end time");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[2u].isObject());
+ EXPECT_EQ_JSON(data[2u]["key"], String, "queueLength");
+ EXPECT_EQ_JSON(data[2u]["label"], String, "Queue length");
+ EXPECT_EQ_JSON(data[2u]["format"], String, "integer");
+
+ } else if (nameString == "VideoFallingBehind") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 2u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "videoFrameStartTimeUs");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Video frame start time");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "mediaCurrentTimeUs");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Media current time");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
+
+ } else if (nameString == "Budget") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "markers-gtest") {
+ EXPECT_EQ(display.size(), 7u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+ EXPECT_EQ(display[2u].asString(), "timeline-overview");
+ EXPECT_EQ(display[3u].asString(), "timeline-memory");
+ EXPECT_EQ(display[4u].asString(), "timeline-ipc");
+ EXPECT_EQ(display[5u].asString(), "timeline-fileio");
+ EXPECT_EQ(display[6u].asString(), "stack-chart");
+
+ EXPECT_EQ_JSON(schema["chartLabel"], String, "chart label");
+ EXPECT_EQ_JSON(schema["tooltipLabel"], String, "tooltip label");
+ EXPECT_EQ_JSON(schema["tableLabel"], String, "table label");
+
+ ASSERT_EQ(data.size(), 14u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "key with url");
+ EXPECT_TRUE(data[0u]["label"].isNull());
+ EXPECT_EQ_JSON(data[0u]["format"], String, "url");
+ EXPECT_TRUE(data[0u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "key with label filePath");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "label filePath");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "file-path");
+ EXPECT_TRUE(data[1u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[2u].isObject());
+ EXPECT_EQ_JSON(data[2u]["key"], String,
+ "key with string not-searchable");
+ EXPECT_TRUE(data[2u]["label"].isNull());
+ EXPECT_EQ_JSON(data[2u]["format"], String, "string");
+ EXPECT_EQ_JSON(data[2u]["searchable"], Bool, false);
+
+ ASSERT_TRUE(data[3u].isObject());
+ EXPECT_EQ_JSON(data[3u]["key"], String,
+ "key with label duration searchable");
+ EXPECT_TRUE(data[3u]["label duration"].isNull());
+ EXPECT_EQ_JSON(data[3u]["format"], String, "duration");
+ EXPECT_EQ_JSON(data[3u]["searchable"], Bool, true);
+
+ ASSERT_TRUE(data[4u].isObject());
+ EXPECT_EQ_JSON(data[4u]["key"], String, "key with time");
+ EXPECT_TRUE(data[4u]["label"].isNull());
+ EXPECT_EQ_JSON(data[4u]["format"], String, "time");
+ EXPECT_TRUE(data[4u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[5u].isObject());
+ EXPECT_EQ_JSON(data[5u]["key"], String, "key with seconds");
+ EXPECT_TRUE(data[5u]["label"].isNull());
+ EXPECT_EQ_JSON(data[5u]["format"], String, "seconds");
+ EXPECT_TRUE(data[5u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[6u].isObject());
+ EXPECT_EQ_JSON(data[6u]["key"], String, "key with milliseconds");
+ EXPECT_TRUE(data[6u]["label"].isNull());
+ EXPECT_EQ_JSON(data[6u]["format"], String, "milliseconds");
+ EXPECT_TRUE(data[6u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[7u].isObject());
+ EXPECT_EQ_JSON(data[7u]["key"], String, "key with microseconds");
+ EXPECT_TRUE(data[7u]["label"].isNull());
+ EXPECT_EQ_JSON(data[7u]["format"], String, "microseconds");
+ EXPECT_TRUE(data[7u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[8u].isObject());
+ EXPECT_EQ_JSON(data[8u]["key"], String, "key with nanoseconds");
+ EXPECT_TRUE(data[8u]["label"].isNull());
+ EXPECT_EQ_JSON(data[8u]["format"], String, "nanoseconds");
+ EXPECT_TRUE(data[8u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[9u].isObject());
+ EXPECT_EQ_JSON(data[9u]["key"], String, "key with bytes");
+ EXPECT_TRUE(data[9u]["label"].isNull());
+ EXPECT_EQ_JSON(data[9u]["format"], String, "bytes");
+ EXPECT_TRUE(data[9u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[10u].isObject());
+ EXPECT_EQ_JSON(data[10u]["key"], String, "key with percentage");
+ EXPECT_TRUE(data[10u]["label"].isNull());
+ EXPECT_EQ_JSON(data[10u]["format"], String, "percentage");
+ EXPECT_TRUE(data[10u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[11u].isObject());
+ EXPECT_EQ_JSON(data[11u]["key"], String, "key with integer");
+ EXPECT_TRUE(data[11u]["label"].isNull());
+ EXPECT_EQ_JSON(data[11u]["format"], String, "integer");
+ EXPECT_TRUE(data[11u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[12u].isObject());
+ EXPECT_EQ_JSON(data[12u]["key"], String, "key with decimal");
+ EXPECT_TRUE(data[12u]["label"].isNull());
+ EXPECT_EQ_JSON(data[12u]["format"], String, "decimal");
+ EXPECT_TRUE(data[12u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[13u].isObject());
+ EXPECT_EQ_JSON(data[13u]["label"], String, "static label");
+ EXPECT_EQ_JSON(data[13u]["value"], String, "static value");
+
+ } else if (nameString == "markers-gtest-special") {
+ EXPECT_EQ(display.size(), 0u);
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "markers-gtest-unused") {
+ ADD_FAILURE() << "Schema for GtestUnusedMarker should not be here";
+
+ } else {
+ printf("FYI: Unknown marker schema '%s'\n", nameString.c_str());
+ }
+ }
+
+ // Check that we've got all expected schema.
+ EXPECT_TRUE(testedSchemaNames.find("Text") != testedSchemaNames.end());
+ EXPECT_TRUE(testedSchemaNames.find("tracing") !=
+ testedSchemaNames.end());
+ EXPECT_TRUE(testedSchemaNames.find("MediaSample") !=
+ testedSchemaNames.end());
+ } // markerSchema
+ } // meta
+ });
+
+ Maybe<ProfilerBufferInfo> info = profiler_get_buffer_info();
+ EXPECT_TRUE(info.isSome());
+ printf("Profiler buffer range: %llu .. %llu (%llu bytes)\n",
+ static_cast<unsigned long long>(info->mRangeStart),
+ static_cast<unsigned long long>(info->mRangeEnd),
+ // sizeof(ProfileBufferEntry) == 9
+ (static_cast<unsigned long long>(info->mRangeEnd) -
+ static_cast<unsigned long long>(info->mRangeStart)) *
+ 9);
+ printf("Stats: min(us) .. mean(us) .. max(us) [count]\n");
+ printf("- Intervals: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mIntervalsUs.min, info->mIntervalsUs.sum / info->mIntervalsUs.n,
+ info->mIntervalsUs.max, info->mIntervalsUs.n);
+ printf("- Overheads: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mOverheadsUs.min, info->mOverheadsUs.sum / info->mOverheadsUs.n,
+ info->mOverheadsUs.max, info->mOverheadsUs.n);
+ printf(" - Locking: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mLockingsUs.min, info->mLockingsUs.sum / info->mLockingsUs.n,
+ info->mLockingsUs.max, info->mLockingsUs.n);
+ printf(" - Clearning: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mCleaningsUs.min, info->mCleaningsUs.sum / info->mCleaningsUs.n,
+ info->mCleaningsUs.max, info->mCleaningsUs.n);
+ printf(" - Counters: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mCountersUs.min, info->mCountersUs.sum / info->mCountersUs.n,
+ info->mCountersUs.max, info->mCountersUs.n);
+ printf(" - Threads: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mThreadsUs.min, info->mThreadsUs.sum / info->mThreadsUs.n,
+ info->mThreadsUs.max, info->mThreadsUs.n);
+
+ profiler_stop();
+
+ // Try to add markers while the profiler is stopped.
+ PROFILER_MARKER_UNTYPED("marker after profiler_stop", OTHER);
+
+ // Warning: this could be racy
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // This last marker shouldn't get streamed.
+ SpliceableChunkedJSONWriter w2{FailureLatchInfallibleSource::Singleton()};
+ w2.Start();
+ EXPECT_TRUE(::profiler_stream_json_for_this_process(w2));
+ w2.End();
+ EXPECT_FALSE(w2.Failed());
+ UniquePtr<char[]> profile2 = w2.ChunkedWriteFunc().CopyData();
+ ASSERT_TRUE(!!profile2.get());
+ EXPECT_TRUE(
+ std::string_view(profile2.get()).find("marker after profiler_stop") ==
+ std::string_view::npos);
+
+ profiler_stop();
+}
+
+# define COUNTER_NAME "TestCounter"
+# define COUNTER_DESCRIPTION "Test of counters in profiles"
+# define COUNTER_NAME2 "Counter2"
+# define COUNTER_DESCRIPTION2 "Second Test of counters in profiles"
+
+PROFILER_DEFINE_COUNT_TOTAL(TestCounter, COUNTER_NAME, COUNTER_DESCRIPTION);
+PROFILER_DEFINE_COUNT_TOTAL(TestCounter2, COUNTER_NAME2, COUNTER_DESCRIPTION2);
+
+TEST(GeckoProfiler, Counters)
+{
+ uint32_t features = 0;
+ const char* filters[] = {"GeckoMain"};
+
+ // We will record some counter values, and check that they're present (and no
+ // other) when expected.
+
+ struct NumberAndCount {
+ uint64_t mNumber;
+ int64_t mCount;
+ };
+
+ int64_t testCounters[] = {10, 7, -17};
+ NumberAndCount expectedTestCounters[] = {{1u, 10}, {0u, 0}, {1u, 7},
+ {0u, 0}, {0u, 0}, {1u, -17},
+ {0u, 0}, {0u, 0}};
+ constexpr size_t expectedTestCountersCount =
+ MOZ_ARRAY_LENGTH(expectedTestCounters);
+
+ bool expectCounter2 = false;
+ int64_t testCounters2[] = {10};
+ NumberAndCount expectedTestCounters2[] = {{1u, 10}, {0u, 0}};
+ constexpr size_t expectedTestCounters2Count =
+ MOZ_ARRAY_LENGTH(expectedTestCounters2);
+
+ auto checkCountersInJSON = [&](const Json::Value& aRoot) {
+ size_t nextExpectedTestCounter = 0u;
+ size_t nextExpectedTestCounter2 = 0u;
+
+ GET_JSON(counters, aRoot["counters"], Array);
+ for (const Json::Value& counter : counters) {
+ ASSERT_TRUE(counter.isObject());
+ GET_JSON_VALUE(name, counter["name"], String);
+ if (name == "TestCounter") {
+ EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME);
+ EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_EQ_JSON(sampleGroup["id"], UInt, 0u);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ ASSERT_LT(nextExpectedTestCounter, expectedTestCountersCount);
+ EXPECT_EQ_JSON(
+ sample[samplesNumber], UInt64,
+ expectedTestCounters[nextExpectedTestCounter].mNumber);
+ EXPECT_EQ_JSON(
+ sample[samplesCount], Int64,
+ expectedTestCounters[nextExpectedTestCounter].mCount);
+ ++nextExpectedTestCounter;
+ }
+ }
+ } else if (name == "TestCounter2") {
+ EXPECT_TRUE(expectCounter2);
+
+ EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME2);
+ EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION2);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_EQ_JSON(sampleGroup["id"], UInt, 0u);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ ASSERT_LT(nextExpectedTestCounter2, expectedTestCounters2Count);
+ EXPECT_EQ_JSON(
+ sample[samplesNumber], UInt64,
+ expectedTestCounters2[nextExpectedTestCounter2].mNumber);
+ EXPECT_EQ_JSON(
+ sample[samplesCount], Int64,
+ expectedTestCounters2[nextExpectedTestCounter2].mCount);
+ ++nextExpectedTestCounter2;
+ }
+ }
+ }
+ }
+
+ EXPECT_EQ(nextExpectedTestCounter, expectedTestCountersCount);
+ if (expectCounter2) {
+ EXPECT_EQ(nextExpectedTestCounter2, expectedTestCounters2Count);
+ }
+ };
+
+ // Inactive -> Active
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Output all "TestCounter"s, with increasing delays (to test different
+ // number of counter samplings).
+ int samplingWaits = 2;
+ for (int64_t counter : testCounters) {
+ AUTO_PROFILER_COUNT_TOTAL(TestCounter, counter);
+ for (int i = 0; i < samplingWaits; ++i) {
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ ++samplingWaits;
+ }
+
+ // Verify we got "TestCounter" in the output, but not "TestCounter2" yet.
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), checkCountersInJSON);
+
+ // Now introduce TestCounter2.
+ expectCounter2 = true;
+ for (int64_t counter2 : testCounters2) {
+ AUTO_PROFILER_COUNT_TOTAL(TestCounter2, counter2);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ // Verify we got both "TestCounter" and "TestCounter2" in the output.
+ profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), checkCountersInJSON);
+
+ profiler_stop();
+}
+
+TEST(GeckoProfiler, Time)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ double t1 = profiler_time();
+ double t2 = profiler_time();
+ ASSERT_TRUE(t1 <= t2);
+
+ // profiler_start() restarts the timer used by profiler_time().
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ double t3 = profiler_time();
+ double t4 = profiler_time();
+ ASSERT_TRUE(t3 <= t4);
+
+ profiler_stop();
+
+ double t5 = profiler_time();
+ double t6 = profiler_time();
+ ASSERT_TRUE(t4 <= t5 && t1 <= t6);
+}
+
+TEST(GeckoProfiler, GetProfile)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_get_profile());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ mozilla::Maybe<uint32_t> activeFeatures = profiler_features_if_active();
+ ASSERT_TRUE(activeFeatures.isSome());
+ // Not all platforms support stack-walking.
+ const bool hasStackWalk = ProfilerFeature::HasStackWalk(*activeFeatures);
+
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
+ GET_JSON(meta, aRoot["meta"], Object);
+ {
+ GET_JSON(configuration, meta["configuration"], Object);
+ {
+ GET_JSON(features, configuration["features"], Array);
+ {
+ EXPECT_EQ(features.size(), (hasStackWalk ? 1u : 0u));
+ if (hasStackWalk) {
+ EXPECT_JSON_ARRAY_CONTAINS(features, String, "stackwalk");
+ }
+ }
+ GET_JSON(threads, configuration["threads"], Array);
+ {
+ EXPECT_EQ(threads.size(), 1u);
+ EXPECT_JSON_ARRAY_CONTAINS(threads, String, "GeckoMain");
+ }
+ }
+ }
+ });
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_get_profile());
+}
+
+TEST(GeckoProfiler, StreamJSONForThisProcess)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(
+ &std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(!w.Fallible());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+ MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+
+ ASSERT_TRUE(!::profiler_stream_json_for_this_process(w));
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ w.Start();
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w));
+ w.End();
+
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+
+ JSONOutputCheck(profile.get(), [](const Json::Value&) {});
+
+ profiler_stop();
+
+ ASSERT_TRUE(!::profiler_stream_json_for_this_process(w));
+}
+
+// Internal version of profiler_stream_json_for_this_process, which allows being
+// called from a non-main thread of the parent process, at the risk of getting
+// an incomplete profile.
+bool do_profiler_stream_json_for_this_process(
+ SpliceableJSONWriter& aWriter, double aSinceTime, bool aIsShuttingDown,
+ ProfilerCodeAddressService* aService,
+ mozilla::ProgressLogger aProgressLogger);
+
+TEST(GeckoProfiler, StreamJSONForThisProcessThreaded)
+{
+ // Same as the previous test, but calling some things on background threads.
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(
+ &std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(!w.Fallible());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+ MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+
+ ASSERT_TRUE(!::profiler_stream_json_for_this_process(w));
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ // Start the profiler on the main thread.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Call profiler_stream_json_for_this_process on a background thread.
+ thread->Dispatch(
+ NS_NewRunnableFunction(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
+ [&]() {
+ w.Start();
+ ASSERT_TRUE(::do_profiler_stream_json_for_this_process(
+ w, /* double aSinceTime */ 0.0,
+ /* bool aIsShuttingDown */ false,
+ /* ProfilerCodeAddressService* aService */ nullptr,
+ mozilla::ProgressLogger{}));
+ w.End();
+ }),
+ NS_DISPATCH_SYNC);
+
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+
+ JSONOutputCheck(profile.get(), [](const Json::Value&) {});
+
+ // Stop the profiler and call profiler_stream_json_for_this_process on a
+ // background thread.
+ thread->Dispatch(
+ NS_NewRunnableFunction(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
+ [&]() {
+ profiler_stop();
+ ASSERT_TRUE(!::do_profiler_stream_json_for_this_process(
+ w, /* double aSinceTime */ 0.0,
+ /* bool aIsShuttingDown */ false,
+ /* ProfilerCodeAddressService* aService */ nullptr,
+ mozilla::ProgressLogger{}));
+ }),
+ NS_DISPATCH_SYNC);
+ thread->Shutdown();
+
+ // Call profiler_stream_json_for_this_process on the main thread.
+ ASSERT_TRUE(!::profiler_stream_json_for_this_process(w));
+}
+
+TEST(GeckoProfiler, ProfilingStack)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ AUTO_PROFILER_LABEL("A::B", OTHER);
+
+ UniqueFreePtr<char> dynamic(strdup("dynamic"));
+ {
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("A::C", JS, dynamic.get());
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("A::C2", JS,
+ nsDependentCString(dynamic.get()));
+ AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING(
+ "A::C3", JS, NS_ConvertUTF8toUTF16(dynamic.get()));
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_get_backtrace());
+ }
+
+ AutoProfilerLabel label1("A", nullptr, JS::ProfilingCategoryPair::DOM);
+ AutoProfilerLabel label2("A", dynamic.get(),
+ JS::ProfilingCategoryPair::NETWORK);
+ ASSERT_TRUE(profiler_get_backtrace());
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_get_profile());
+}
+
+TEST(GeckoProfiler, Bug1355807)
+{
+ uint32_t features = ProfilerFeature::JS;
+ const char* manyThreadsFilter[] = {""};
+ const char* fewThreadsFilter[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ manyThreadsFilter, MOZ_ARRAY_LENGTH(manyThreadsFilter), 0);
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ fewThreadsFilter, MOZ_ARRAY_LENGTH(fewThreadsFilter), 0);
+
+ // In bug 1355807 this caused an assertion failure in StopJSSampling().
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ fewThreadsFilter, MOZ_ARRAY_LENGTH(fewThreadsFilter), 0);
+
+ profiler_stop();
+}
+
+class GTestStackCollector final : public ProfilerStackCollector {
+ public:
+ GTestStackCollector() : mSetIsMainThread(0), mFrames(0) {}
+
+ virtual void SetIsMainThread() { mSetIsMainThread++; }
+
+ virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; }
+ virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; }
+ virtual void CollectWasmFrame(const char* aLabel) { mFrames++; }
+ virtual void CollectProfilingStackFrame(
+ const js::ProfilingStackFrame& aFrame) {
+ mFrames++;
+ }
+
+ int mSetIsMainThread;
+ int mFrames;
+};
+
+void DoSuspendAndSample(ProfilerThreadId aTidToSample,
+ nsIThread* aSamplingThread) {
+ aSamplingThread->Dispatch(
+ NS_NewRunnableFunction(
+ "GeckoProfiler_SuspendAndSample_Test::TestBody",
+ [&]() {
+ uint32_t features = ProfilerFeature::CPUUtilization;
+ GTestStackCollector collector;
+ profiler_suspend_and_sample_thread(aTidToSample, features,
+ collector,
+ /* sampleNative = */ true);
+
+ ASSERT_TRUE(collector.mSetIsMainThread ==
+ (aTidToSample == profiler_main_thread_id()));
+ ASSERT_TRUE(collector.mFrames > 0);
+ }),
+ NS_DISPATCH_SYNC);
+}
+
+TEST(GeckoProfiler, SuspendAndSample)
+{
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ ProfilerThreadId tid = profiler_current_thread_id();
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ // Suspend and sample while the profiler is inactive.
+ DoSuspendAndSample(tid, thread);
+
+ DoSuspendAndSample(ProfilerThreadId{}, thread);
+
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_is_active());
+
+ // Suspend and sample while the profiler is active.
+ DoSuspendAndSample(tid, thread);
+
+ DoSuspendAndSample(ProfilerThreadId{}, thread);
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_is_active());
+}
+
+TEST(GeckoProfiler, PostSamplingCallback)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+ {
+ // Stack sampling -> This label should appear at least once.
+ AUTO_PROFILER_LABEL("PostSamplingCallback completed", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ UniquePtr<char[]> profileCompleted = profiler_get_profile();
+ JSONOutputCheck(profileCompleted.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ GET_JSON(thread0, threads[0], Object);
+ {
+ EXPECT_JSON_ARRAY_CONTAINS(thread0["stringTable"], String,
+ "PostSamplingCallback completed");
+ }
+ }
+ });
+
+ profiler_pause();
+ {
+ // Paused -> This label should not appear.
+ AUTO_PROFILER_LABEL("PostSamplingCallback paused", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingPaused);
+ }
+ UniquePtr<char[]> profilePaused = profiler_get_profile();
+ JSONOutputCheck(profilePaused.get(), [](const Json::Value& aRoot) {});
+ // This string shouldn't appear *anywhere* in the profile.
+ ASSERT_FALSE(strstr(profilePaused.get(), "PostSamplingCallback paused"));
+
+ profiler_resume();
+ {
+ // Stack sampling -> This label should appear at least once.
+ AUTO_PROFILER_LABEL("PostSamplingCallback resumed", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ UniquePtr<char[]> profileResumed = profiler_get_profile();
+ JSONOutputCheck(profileResumed.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ GET_JSON(thread0, threads[0], Object);
+ {
+ EXPECT_JSON_ARRAY_CONTAINS(thread0["stringTable"], String,
+ "PostSamplingCallback resumed");
+ }
+ }
+ });
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::NoStackSampling,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ {
+ // No stack sampling -> This label should not appear.
+ AUTO_PROFILER_LABEL("PostSamplingCallback completed (no stacks)", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::NoStackSamplingCompleted);
+ }
+ UniquePtr<char[]> profileNoStacks = profiler_get_profile();
+ JSONOutputCheck(profileNoStacks.get(), [](const Json::Value& aRoot) {});
+ // This string shouldn't appear *anywhere* in the profile.
+ ASSERT_FALSE(strstr(profileNoStacks.get(),
+ "PostSamplingCallback completed (no stacks)"));
+
+ // Note: There is no non-racy way to test for SamplingState::JustStopped, as
+ // it would require coordination between `profiler_stop()` and another thread
+ // doing `profiler_callback_after_sampling()` at just the right moment.
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+}
+
+TEST(GeckoProfiler, ProfilingStateCallback)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ struct ProfilingStateAndId {
+ ProfilingState mProfilingState;
+ int mId;
+ };
+ DataMutex<Vector<ProfilingStateAndId>> states{"Profiling states"};
+ auto CreateCallback = [&states](int id) {
+ return [id, &states](ProfilingState aProfilingState) {
+ auto lockedStates = states.Lock();
+ ASSERT_TRUE(
+ lockedStates->append(ProfilingStateAndId{aProfilingState, id}));
+ };
+ };
+ auto CheckStatesIsEmpty = [&states]() {
+ auto lockedStates = states.Lock();
+ EXPECT_TRUE(lockedStates->empty());
+ };
+ auto CheckStatesOnlyContains = [&states](ProfilingState aProfilingState,
+ int aId) {
+ auto lockedStates = states.Lock();
+ EXPECT_EQ(lockedStates->length(), 1u);
+ if (lockedStates->length() >= 1u) {
+ EXPECT_EQ((*lockedStates)[0].mProfilingState, aProfilingState);
+ EXPECT_EQ((*lockedStates)[0].mId, aId);
+ }
+ lockedStates->clear();
+ };
+
+ profiler_add_state_change_callback(AllProfilingStates(), CreateCallback(1),
+ 1);
+ // This is in case of error, and it also exercises the (allowed) removal of
+ // unknown callback ids.
+ auto cleanup1 = mozilla::MakeScopeExit(
+ []() { profiler_remove_state_change_callback(1); });
+ CheckStatesIsEmpty();
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+
+ CheckStatesOnlyContains(ProfilingState::Started, 1);
+
+ profiler_add_state_change_callback(AllProfilingStates(), CreateCallback(2),
+ 2);
+ // This is in case of error, and it also exercises the (allowed) removal of
+ // unknown callback ids.
+ auto cleanup2 = mozilla::MakeScopeExit(
+ []() { profiler_remove_state_change_callback(2); });
+ CheckStatesOnlyContains(ProfilingState::AlreadyActive, 2);
+
+ profiler_remove_state_change_callback(2);
+ CheckStatesOnlyContains(ProfilingState::RemovingCallback, 2);
+ // Note: The actual removal is effectively tested below, by not seeing any
+ // more invocations of the 2nd callback.
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ UniquePtr<char[]> profileCompleted = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileCompleted.get(), [](const Json::Value& aRoot) {});
+
+ profiler_pause();
+ CheckStatesOnlyContains(ProfilingState::Pausing, 1);
+ UniquePtr<char[]> profilePaused = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profilePaused.get(), [](const Json::Value& aRoot) {});
+
+ profiler_resume();
+ CheckStatesOnlyContains(ProfilingState::Resumed, 1);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ UniquePtr<char[]> profileResumed = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileResumed.get(), [](const Json::Value& aRoot) {});
+
+ // This effectively stops the profiler before restarting it, but
+ // ProfilingState::Stopping is not notified. See `profiler_start` for details.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::NoStackSampling,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ CheckStatesOnlyContains(ProfilingState::Started, 1);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::NoStackSamplingCompleted);
+ UniquePtr<char[]> profileNoStacks = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileNoStacks.get(), [](const Json::Value& aRoot) {});
+
+ profiler_stop();
+ CheckStatesOnlyContains(ProfilingState::Stopping, 1);
+ ASSERT_TRUE(!profiler_is_active());
+
+ profiler_remove_state_change_callback(1);
+ CheckStatesOnlyContains(ProfilingState::RemovingCallback, 1);
+
+ // Note: ProfilingState::ShuttingDown cannot be tested here, and the profiler
+ // can only be shut down once per process.
+}
+
+TEST(GeckoProfiler, BaseProfilerHandOff)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!baseprofiler::profiler_is_active());
+ ASSERT_TRUE(!profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker before base profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker before base profiler", OTHER, {});
+
+ // Start the Base Profiler.
+ baseprofiler::profiler_start(
+ PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters));
+
+ ASSERT_TRUE(baseprofiler::profiler_is_active());
+ ASSERT_TRUE(!profiler_is_active());
+
+ // Add at least a marker, which should go straight into the buffer.
+ Maybe<baseprofiler::ProfilerBufferInfo> info0 =
+ baseprofiler::profiler_get_buffer_info();
+ BASE_PROFILER_MARKER_UNTYPED("Base marker during base profiler", OTHER, {});
+ Maybe<baseprofiler::ProfilerBufferInfo> info1 =
+ baseprofiler::profiler_get_buffer_info();
+ ASSERT_GT(info1->mRangeEnd, info0->mRangeEnd);
+
+ PROFILER_MARKER_UNTYPED("Gecko marker during base profiler", OTHER, {});
+
+ // Start the Gecko Profiler, which should grab the Base Profiler profile and
+ // stop it.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+
+ ASSERT_TRUE(!baseprofiler::profiler_is_active());
+ ASSERT_TRUE(profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker during gecko profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker during gecko profiler", OTHER, {});
+
+ // Write some Gecko Profiler samples.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // Check that the Gecko Profiler profile contains at least the Base Profiler
+ // main thread samples.
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker after gecko profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker after gecko profiler", OTHER, {});
+
+ JSONOutputCheck(profile.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ bool found = false;
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ found = true;
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Base marker before base profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker before base profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Base marker during base profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker during base profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Base marker during gecko profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Gecko marker during gecko profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Base marker after gecko profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker after gecko profiler");
+ break;
+ }
+ }
+ EXPECT_TRUE(found);
+ }
+ });
+}
+
+static std::string_view GetFeatureName(uint32_t feature) {
+ switch (feature) {
+# define FEATURE_NAME(n_, str_, Name_, desc_) \
+ case ProfilerFeature::Name_: \
+ return str_;
+
+ PROFILER_FOR_EACH_FEATURE(FEATURE_NAME)
+
+# undef FEATURE_NAME
+
+ default:
+ return "?";
+ }
+}
+
+TEST(GeckoProfiler, FeatureCombinations)
+{
+ const char* filters[] = {"*"};
+
+ // List of features to test. Every combination of up to 3 of them will be
+ // tested, so be careful not to add too many to keep the test run at a
+ // reasonable time.
+ uint32_t featureList[] = {ProfilerFeature::JS,
+ ProfilerFeature::Screenshots,
+ ProfilerFeature::StackWalk,
+ ProfilerFeature::NoStackSampling,
+ ProfilerFeature::NativeAllocations,
+ ProfilerFeature::CPUUtilization,
+ ProfilerFeature::CPUAllThreads,
+ ProfilerFeature::SamplingAllThreads,
+ ProfilerFeature::MarkersAllThreads,
+ ProfilerFeature::UnregisteredThreads};
+ constexpr uint32_t featureCount = uint32_t(MOZ_ARRAY_LENGTH(featureList));
+
+ auto testFeatures = [&](uint32_t features,
+ const std::string& featuresString) {
+ SCOPED_TRACE(featuresString.c_str());
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_is_active());
+
+ // Write some Gecko Profiler samples.
+ EXPECT_EQ(WaitForSamplingState(),
+ (((features & ProfilerFeature::NoStackSampling) != 0) &&
+ ((features & (ProfilerFeature::CPUUtilization |
+ ProfilerFeature::CPUAllThreads)) == 0))
+ ? SamplingState::NoStackSamplingCompleted
+ : SamplingState::SamplingCompleted);
+
+ // Check that the profile looks valid. Note that we don't test feature-
+ // specific changes.
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), [](const Json::Value& aRoot) {});
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ };
+
+ testFeatures(0, "Features: (none)");
+
+ for (uint32_t f1 = 0u; f1 < featureCount; ++f1) {
+ const uint32_t features1 = featureList[f1];
+ std::string features1String = "Features: ";
+ features1String += GetFeatureName(featureList[f1]);
+
+ testFeatures(features1, features1String);
+
+ for (uint32_t f2 = f1 + 1u; f2 < featureCount; ++f2) {
+ const uint32_t features12 = f1 | featureList[f2];
+ std::string features12String = features1String + " ";
+ features12String += GetFeatureName(featureList[f2]);
+
+ testFeatures(features12, features12String);
+
+ for (uint32_t f3 = f2 + 1u; f3 < featureCount; ++f3) {
+ const uint32_t features123 = features12 | featureList[f3];
+ std::string features123String = features12String + " ";
+ features123String += GetFeatureName(featureList[f3]);
+
+ testFeatures(features123, features123String);
+ }
+ }
+ }
+}
+
+static void CountCPUDeltas(const Json::Value& aThread, size_t& aOutSamplings,
+ uint64_t& aOutCPUDeltaSum) {
+ GET_JSON(samples, aThread["samples"], Object);
+ {
+ Json::ArrayIndex threadCPUDeltaIndex = 0;
+ GET_JSON(schema, samples["schema"], Object);
+ {
+ GET_JSON(jsonThreadCPUDeltaIndex, schema["threadCPUDelta"], UInt);
+ threadCPUDeltaIndex = jsonThreadCPUDeltaIndex.asUInt();
+ }
+
+ aOutSamplings = 0;
+ aOutCPUDeltaSum = 0;
+ GET_JSON(data, samples["data"], Array);
+ aOutSamplings = data.size();
+ for (const Json::Value& sample : data) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(threadCPUDeltaIndex)) {
+ if (!sample[threadCPUDeltaIndex].isNull()) {
+ GET_JSON(cpuDelta, sample[threadCPUDeltaIndex], UInt64);
+ aOutCPUDeltaSum += uint64_t(cpuDelta.asUInt64());
+ }
+ }
+ }
+ }
+}
+
+TEST(GeckoProfiler, CPUUsage)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ const char* filters[] = {"GeckoMain", "Idle test", "Busy test"};
+
+ enum class TestThreadsState {
+ // Initial state, while constructing and starting the idle thread.
+ STARTING,
+ // Set by the idle thread just before running its main mostly-idle loop.
+ RUNNING1,
+ RUNNING2,
+ // Set by the main thread when it wants the idle thread to stop.
+ STOPPING
+ };
+ Atomic<TestThreadsState> testThreadsState{TestThreadsState::STARTING};
+
+ std::thread idle([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Idle test");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Idle test", PROFILER);
+ ASSERT_TRUE(testThreadsState.compareExchange(TestThreadsState::STARTING,
+ TestThreadsState::RUNNING1) ||
+ testThreadsState.compareExchange(TestThreadsState::RUNNING1,
+ TestThreadsState::RUNNING2));
+
+ while (testThreadsState != TestThreadsState::STOPPING) {
+ // Sleep for multiple profiler intervals, so the profiler should have
+ // samples with zero CPU utilization.
+ PR_Sleep(PR_MillisecondsToInterval(PROFILER_DEFAULT_INTERVAL * 10));
+ }
+ });
+
+ std::thread busy([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Busy test");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Busy test", PROFILER);
+ ASSERT_TRUE(testThreadsState.compareExchange(TestThreadsState::STARTING,
+ TestThreadsState::RUNNING1) ||
+ testThreadsState.compareExchange(TestThreadsState::RUNNING1,
+ TestThreadsState::RUNNING2));
+
+ while (testThreadsState != TestThreadsState::STOPPING) {
+ // Stay busy!
+ }
+ });
+
+ // Wait for idle thread to start running its main loop.
+ while (testThreadsState != TestThreadsState::RUNNING2) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+
+ // We want to ensure that CPU usage numbers are present whether or not we are
+ // collecting stack samples.
+ static constexpr bool scTestsWithOrWithoutStackSampling[] = {false, true};
+ for (const bool testWithNoStackSampling : scTestsWithOrWithoutStackSampling) {
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+
+ profiler_start(
+ PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::CPUUtilization |
+ (testWithNoStackSampling ? ProfilerFeature::NoStackSampling : 0),
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ // Grab a few samples, each with a different label on the stack.
+# define SAMPLE_LABEL_PREFIX "CPUUsage sample label "
+ static constexpr const char* scSampleLabels[] = {
+ SAMPLE_LABEL_PREFIX "0", SAMPLE_LABEL_PREFIX "1",
+ SAMPLE_LABEL_PREFIX "2", SAMPLE_LABEL_PREFIX "3",
+ SAMPLE_LABEL_PREFIX "4", SAMPLE_LABEL_PREFIX "5",
+ SAMPLE_LABEL_PREFIX "6", SAMPLE_LABEL_PREFIX "7",
+ SAMPLE_LABEL_PREFIX "8", SAMPLE_LABEL_PREFIX "9"};
+ static constexpr size_t scSampleLabelCount =
+ (sizeof(scSampleLabels) / sizeof(scSampleLabels[0]));
+ // We'll do two samplings for each label.
+ static constexpr size_t scMinSamplings = scSampleLabelCount * 2;
+
+ for (const char* sampleLabel : scSampleLabels) {
+ AUTO_PROFILER_LABEL(sampleLabel, OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ // Note: There could have been a delay before this label above, where the
+ // profiler could have sampled the stack and missed the label. By forcing
+ // another sampling now, the label is guaranteed to be present.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ if (testWithNoStackSampling) {
+ // If we are testing nostacksampling, we shouldn't find this label prefix
+ // in the profile.
+ EXPECT_FALSE(strstr(profile.get(), SAMPLE_LABEL_PREFIX));
+ } else {
+ // In normal sampling mode, we should find all labels.
+ for (const char* sampleLabel : scSampleLabels) {
+ EXPECT_TRUE(strstr(profile.get(), sampleLabel));
+ }
+ }
+
+ JSONOutputCheck(profile.get(), [testWithNoStackSampling](
+ const Json::Value& aRoot) {
+ // Check that the "cpu" feature is present.
+ GET_JSON(meta, aRoot["meta"], Object);
+ {
+ GET_JSON(configuration, meta["configuration"], Object);
+ {
+ GET_JSON(features, configuration["features"], Array);
+ { EXPECT_JSON_ARRAY_CONTAINS(features, String, "cpu"); }
+ }
+ }
+
+ {
+ GET_JSON(sampleUnits, meta["sampleUnits"], Object);
+ {
+ EXPECT_EQ_JSON(sampleUnits["time"], String, "ms");
+ EXPECT_EQ_JSON(sampleUnits["eventDelay"], String, "ms");
+# if defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || defined(GP_OS_freebsd)
+ // Note: The exact string is not important here.
+ EXPECT_TRUE(sampleUnits["threadCPUDelta"].isString())
+ << "There should be a sampleUnits.threadCPUDelta on this "
+ "platform";
+# else
+ EXPECT_FALSE(sampleUnits.isMember("threadCPUDelta"))
+ << "Unexpected sampleUnits.threadCPUDelta on this platform";;
+# endif
+ }
+ }
+
+ bool foundMain = false;
+ bool foundIdle = false;
+ uint64_t idleThreadCPUDeltaSum = 0u;
+ bool foundBusy = false;
+ uint64_t busyThreadCPUDeltaSum = 0u;
+
+ // Check that the sample schema contains "threadCPUDelta".
+ GET_JSON(threads, aRoot["threads"], Array);
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ foundMain = true;
+ GET_JSON(samples, thread["samples"], Object);
+ {
+ Json::ArrayIndex stackIndex = 0;
+ Json::ArrayIndex threadCPUDeltaIndex = 0;
+ GET_JSON(schema, samples["schema"], Object);
+ {
+ GET_JSON(jsonStackIndex, schema["stack"], UInt);
+ stackIndex = jsonStackIndex.asUInt();
+ GET_JSON(jsonThreadCPUDeltaIndex, schema["threadCPUDelta"], UInt);
+ threadCPUDeltaIndex = jsonThreadCPUDeltaIndex.asUInt();
+ }
+
+ std::set<uint64_t> stackLeaves; // To count distinct leaves.
+ unsigned threadCPUDeltaCount = 0;
+ GET_JSON(data, samples["data"], Array);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(data.size(), scMinSamplings - 1);
+ } else {
+ EXPECT_GE(data.size(), scMinSamplings);
+ }
+ for (const Json::Value& sample : data) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(stackIndex)) {
+ if (!sample[stackIndex].isNull()) {
+ GET_JSON(stack, sample[stackIndex], UInt64);
+ stackLeaves.insert(stack.asUInt64());
+ }
+ }
+ if (sample.isValidIndex(threadCPUDeltaIndex)) {
+ if (!sample[threadCPUDeltaIndex].isNull()) {
+ EXPECT_TRUE(sample[threadCPUDeltaIndex].isUInt64());
+ ++threadCPUDeltaCount;
+ }
+ }
+ }
+
+ if (testWithNoStackSampling) {
+ // in nostacksampling mode, there should only be one kind of stack
+ // leaf (the root).
+ EXPECT_EQ(stackLeaves.size(), 1u);
+ } else {
+ // in normal sampling mode, there should be at least one kind of
+ // stack leaf for each distinct label.
+ EXPECT_GE(stackLeaves.size(), scSampleLabelCount);
+ }
+
+# if defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || defined(GP_OS_freebsd)
+ EXPECT_GE(threadCPUDeltaCount, data.size() - 1u)
+ << "There should be 'threadCPUDelta' values in all but 1 "
+ "samples";
+# else
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(threadCPUDeltaCount, 0u);
+# endif
+ }
+ } else if (name.asString() == "Idle test") {
+ foundIdle = true;
+ size_t samplings;
+ CountCPUDeltas(thread, samplings, idleThreadCPUDeltaSum);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(samplings, scMinSamplings - 1);
+ } else {
+ EXPECT_GE(samplings, scMinSamplings);
+ }
+# if !(defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || \
+ defined(GP_OS_freebsd))
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(idleThreadCPUDeltaSum, 0u);
+# endif
+ } else if (name.asString() == "Busy test") {
+ foundBusy = true;
+ size_t samplings;
+ CountCPUDeltas(thread, samplings, busyThreadCPUDeltaSum);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(samplings, scMinSamplings - 1);
+ } else {
+ EXPECT_GE(samplings, scMinSamplings);
+ }
+# if !(defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || \
+ defined(GP_OS_freebsd))
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(busyThreadCPUDeltaSum, 0u);
+# endif
+ }
+ }
+
+ EXPECT_TRUE(foundMain);
+ EXPECT_TRUE(foundIdle);
+ EXPECT_TRUE(foundBusy);
+ EXPECT_LE(idleThreadCPUDeltaSum, busyThreadCPUDeltaSum);
+ });
+
+ // Note: There is no non-racy way to test for SamplingState::JustStopped, as
+ // it would require coordination between `profiler_stop()` and another
+ // thread doing `profiler_callback_after_sampling()` at just the right
+ // moment.
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+ }
+
+ testThreadsState = TestThreadsState::STOPPING;
+ busy.join();
+ idle.join();
+}
+
+TEST(GeckoProfiler, AllThreads)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ ASSERT_EQ(static_cast<uint32_t>(ThreadProfilingFeatures::Any), 1u + 2u + 4u)
+ << "This test assumes that there are 3 binary choices 1+2+4; "
+ "Is this test up to date?";
+
+ for (uint32_t threadFeaturesBinary = 0u;
+ threadFeaturesBinary <=
+ static_cast<uint32_t>(ThreadProfilingFeatures::Any);
+ ++threadFeaturesBinary) {
+ ThreadProfilingFeatures threadFeatures =
+ static_cast<ThreadProfilingFeatures>(threadFeaturesBinary);
+ const bool threadCPU = DoFeaturesIntersect(
+ threadFeatures, ThreadProfilingFeatures::CPUUtilization);
+ const bool threadSampling =
+ DoFeaturesIntersect(threadFeatures, ThreadProfilingFeatures::Sampling);
+ const bool threadMarkers =
+ DoFeaturesIntersect(threadFeatures, ThreadProfilingFeatures::Markers);
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ std::string featuresString = "Features: StackWalk Threads";
+ if (threadCPU) {
+ features |= ProfilerFeature::CPUAllThreads;
+ featuresString += " CPUAllThreads";
+ }
+ if (threadSampling) {
+ features |= ProfilerFeature::SamplingAllThreads;
+ featuresString += " SamplingAllThreads";
+ }
+ if (threadMarkers) {
+ features |= ProfilerFeature::MarkersAllThreads;
+ featuresString += " MarkersAllThreads";
+ }
+
+ SCOPED_TRACE(featuresString.c_str());
+
+ const char* filters[] = {"GeckoMain", "Selected"};
+
+ EXPECT_FALSE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_FALSE(profiler_thread_is_being_profiled_for_markers());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ EXPECT_TRUE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_TRUE(profiler_thread_is_being_profiled_for_markers());
+
+ // This will signal all threads to stop spinning.
+ Atomic<bool> stopThreads{false};
+
+ Atomic<int> selectedThreadSpins{0};
+ std::thread selectedThread([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Selected test thread");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Selected test thread", PROFILER);
+ EXPECT_TRUE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_TRUE(profiler_thread_is_being_profiled_for_markers());
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Selected!", PROFILER);
+ ++selectedThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ Atomic<int> unselectedThreadSpins{0};
+ std::thread unselectedThread([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Registered test thread");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Registered test thread", PROFILER);
+ // This thread is *not* selected for full profiling, but it may still be
+ // profiled depending on the -allthreads features.
+ EXPECT_EQ(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization),
+ threadCPU);
+ EXPECT_EQ(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling),
+ threadSampling);
+ EXPECT_EQ(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers),
+ threadMarkers);
+ EXPECT_EQ(profiler_thread_is_being_profiled_for_markers(), threadMarkers);
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Registered!", PROFILER);
+ ++unselectedThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ Atomic<int> unregisteredThreadSpins{0};
+ std::thread unregisteredThread([&]() {
+ // No `AUTO_PROFILER_REGISTER_THREAD` here.
+ EXPECT_FALSE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_FALSE(profiler_thread_is_being_profiled_for_markers());
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Unregistered!", PROFILER);
+ ++unregisteredThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ // Wait for all threads to have started at least one spin.
+ while (selectedThreadSpins == 0 || unselectedThreadSpins == 0 ||
+ unregisteredThreadSpins == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+
+ // Wait until the sampler has done at least one loop.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // Restart the spin counts, and ensure each threads will do at least one
+ // more spin each. Since spins are increased after PROFILER_MARKER calls, in
+ // the worst case, each thread will have attempted to record at least one
+ // marker.
+ selectedThreadSpins = 0;
+ unselectedThreadSpins = 0;
+ unregisteredThreadSpins = 0;
+ while (selectedThreadSpins < 1 && unselectedThreadSpins < 1 &&
+ unregisteredThreadSpins < 1) {
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ profiler_pause();
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ profiler_stop();
+ stopThreads = true;
+ unregisteredThread.join();
+ unselectedThread.join();
+ selectedThread.join();
+
+ JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ int foundMain = 0;
+ int foundSelected = 0;
+ int foundSelectedMarker = 0;
+ int foundUnselected = 0;
+ int foundUnselectedMarker = 0;
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(stringTable, thread["stringTable"], Array);
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ ++foundMain;
+ // Don't check the main thread further in this test.
+
+ } else if (name.asString() == "Selected test thread") {
+ ++foundSelected;
+
+ GET_JSON(samples, thread["samples"], Object);
+ GET_JSON(samplesData, samples["data"], Array);
+ EXPECT_GT(samplesData.size(), 0u);
+
+ GET_JSON(markers, thread["markers"], Object);
+ GET_JSON(markersData, markers["data"], Array);
+ for (const Json::Value& marker : markersData) {
+ const unsigned int NAME = 0u;
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ if (name == "Spinning Selected!") {
+ ++foundSelectedMarker;
+ }
+ }
+ } else if (name.asString() == "Registered test thread") {
+ ++foundUnselected;
+
+ GET_JSON(samples, thread["samples"], Object);
+ GET_JSON(samplesData, samples["data"], Array);
+ if (threadCPU || threadSampling) {
+ EXPECT_GT(samplesData.size(), 0u);
+ } else {
+ EXPECT_EQ(samplesData.size(), 0u);
+ }
+
+ GET_JSON(markers, thread["markers"], Object);
+ GET_JSON(markersData, markers["data"], Array);
+ for (const Json::Value& marker : markersData) {
+ const unsigned int NAME = 0u;
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ if (name == "Spinning Registered!") {
+ ++foundUnselectedMarker;
+ }
+ }
+
+ } else {
+ EXPECT_STRNE(name.asString().c_str(),
+ "Unregistered test thread label");
+ }
+ }
+ EXPECT_EQ(foundMain, 1);
+ EXPECT_EQ(foundSelected, 1);
+ EXPECT_GT(foundSelectedMarker, 0);
+ EXPECT_EQ(foundUnselected,
+ (threadCPU || threadSampling || threadMarkers) ? 1 : 0)
+ << "Unselected thread should only be present if at least one of the "
+ "allthreads feature is on";
+ if (threadMarkers) {
+ EXPECT_GT(foundUnselectedMarker, 0);
+ } else {
+ EXPECT_EQ(foundUnselectedMarker, 0);
+ }
+ });
+ }
+}
+
+TEST(GeckoProfiler, FailureHandling)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // User-defined marker type that generates a failure when streaming JSON.
+ struct GtestFailingMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-failing");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {
+ aWriter.SetFailure("boom!");
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+ EXPECT_TRUE(profiler_add_marker("Gtest failing marker",
+ geckoprofiler::category::OTHER, {},
+ GtestFailingMarker{}));
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ profiler_pause();
+
+ FailureLatchSource failureLatch;
+ SpliceableChunkedJSONWriter w{failureLatch};
+ EXPECT_FALSE(w.Failed());
+ ASSERT_FALSE(w.GetFailure());
+
+ w.Start();
+ EXPECT_FALSE(w.Failed());
+ ASSERT_FALSE(w.GetFailure());
+
+ // The marker will cause a failure during this function call.
+ EXPECT_FALSE(::profiler_stream_json_for_this_process(w));
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ // Already failed, check that we don't crash or reset the failure.
+ EXPECT_FALSE(::profiler_stream_json_for_this_process(w));
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ w.End();
+
+ profiler_stop();
+
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+ ASSERT_EQ(profile.get(), nullptr);
+}
+
+#endif // MOZ_GECKO_PROFILER
diff --git a/tools/profiler/tests/gtest/LulTest.cpp b/tools/profiler/tests/gtest/LulTest.cpp
new file mode 100644
index 0000000000..159a366567
--- /dev/null
+++ b/tools/profiler/tests/gtest/LulTest.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/Atomics.h"
+#include "LulMain.h"
+#include "GeckoProfiler.h" // for TracingKind
+#include "platform-linux-lul.h" // for read_procmaps
+
+// Set this to 0 to make LUL be completely silent during tests.
+// Set it to 1 to get logging output from LUL, presumably for
+// the purpose of debugging it.
+#define DEBUG_LUL_TEST 0
+
+// LUL needs a callback for its logging sink.
+static void gtest_logging_sink_for_LulIntegration(const char* str) {
+ if (DEBUG_LUL_TEST == 0) {
+ return;
+ }
+ // Ignore any trailing \n, since LOG will add one anyway.
+ size_t n = strlen(str);
+ if (n > 0 && str[n - 1] == '\n') {
+ char* tmp = strdup(str);
+ tmp[n - 1] = 0;
+ fprintf(stderr, "LUL-in-gtest: %s\n", tmp);
+ free(tmp);
+ } else {
+ fprintf(stderr, "LUL-in-gtest: %s\n", str);
+ }
+}
+
+TEST(LulIntegration, unwind_consistency)
+{
+ // Set up LUL and get it to read unwind info for libxul.so, which is
+ // all we care about here, plus (incidentally) practically every
+ // other object in the process too.
+ lul::LUL* lul = new lul::LUL(gtest_logging_sink_for_LulIntegration);
+ read_procmaps(lul);
+
+ // Run unwind tests and receive information about how many there
+ // were and how many were successful.
+ lul->EnableUnwinding();
+ int nTests = 0, nTestsPassed = 0;
+ RunLulUnitTests(&nTests, &nTestsPassed, lul);
+ EXPECT_TRUE(nTests == 6) << "Unexpected number of tests";
+ EXPECT_EQ(nTestsPassed, nTests) << "Not all tests passed";
+
+ delete lul;
+}
diff --git a/tools/profiler/tests/gtest/LulTestDwarf.cpp b/tools/profiler/tests/gtest/LulTestDwarf.cpp
new file mode 100644
index 0000000000..55373ec093
--- /dev/null
+++ b/tools/profiler/tests/gtest/LulTestDwarf.cpp
@@ -0,0 +1,2733 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "LulCommonExt.h"
+#include "LulDwarfExt.h"
+#include "LulDwarfInt.h"
+#include "LulTestInfrastructure.h"
+
+using lul_test::CFISection;
+using lul_test::test_assembler::kBigEndian;
+using lul_test::test_assembler::kLittleEndian;
+using lul_test::test_assembler::Label;
+using testing::_;
+using testing::InSequence;
+using testing::Return;
+using testing::Sequence;
+using testing::Test;
+
+#define PERHAPS_WRITE_DEBUG_FRAME_FILE(name, section) /**/
+#define PERHAPS_WRITE_EH_FRAME_FILE(name, section) /**/
+
+// Set this to 0 to make LUL be completely silent during tests.
+// Set it to 1 to get logging output from LUL, presumably for
+// the purpose of debugging it.
+#define DEBUG_LUL_TEST_DWARF 0
+
+// LUL needs a callback for its logging sink.
+static void gtest_logging_sink_for_LulTestDwarf(const char* str) {
+ if (DEBUG_LUL_TEST_DWARF == 0) {
+ return;
+ }
+ // Ignore any trailing \n, since LOG will add one anyway.
+ size_t n = strlen(str);
+ if (n > 0 && str[n - 1] == '\n') {
+ char* tmp = strdup(str);
+ tmp[n - 1] = 0;
+ fprintf(stderr, "LUL-in-gtest: %s\n", tmp);
+ free(tmp);
+ } else {
+ fprintf(stderr, "LUL-in-gtest: %s\n", str);
+ }
+}
+
+namespace lul {
+
+class MockCallFrameInfoHandler : public CallFrameInfo::Handler {
+ public:
+ MOCK_METHOD6(Entry,
+ bool(size_t offset, uint64 address, uint64 length, uint8 version,
+ const std::string& augmentation, unsigned return_address));
+ MOCK_METHOD2(UndefinedRule, bool(uint64 address, int reg));
+ MOCK_METHOD2(SameValueRule, bool(uint64 address, int reg));
+ MOCK_METHOD4(OffsetRule,
+ bool(uint64 address, int reg, int base_register, long offset));
+ MOCK_METHOD4(ValOffsetRule,
+ bool(uint64 address, int reg, int base_register, long offset));
+ MOCK_METHOD3(RegisterRule, bool(uint64 address, int reg, int base_register));
+ MOCK_METHOD3(ExpressionRule,
+ bool(uint64 address, int reg, const ImageSlice& expression));
+ MOCK_METHOD3(ValExpressionRule,
+ bool(uint64 address, int reg, const ImageSlice& expression));
+ MOCK_METHOD0(End, bool());
+ MOCK_METHOD2(PersonalityRoutine, bool(uint64 address, bool indirect));
+ MOCK_METHOD2(LanguageSpecificDataArea, bool(uint64 address, bool indirect));
+ MOCK_METHOD0(SignalHandler, bool());
+};
+
+class MockCallFrameErrorReporter : public CallFrameInfo::Reporter {
+ public:
+ MockCallFrameErrorReporter()
+ : Reporter(gtest_logging_sink_for_LulTestDwarf, "mock filename",
+ "mock section") {}
+ MOCK_METHOD2(Incomplete, void(uint64, CallFrameInfo::EntryKind));
+ MOCK_METHOD1(EarlyEHTerminator, void(uint64));
+ MOCK_METHOD2(CIEPointerOutOfRange, void(uint64, uint64));
+ MOCK_METHOD2(BadCIEId, void(uint64, uint64));
+ MOCK_METHOD2(UnrecognizedVersion, void(uint64, int version));
+ MOCK_METHOD2(UnrecognizedAugmentation, void(uint64, const string&));
+ MOCK_METHOD2(InvalidPointerEncoding, void(uint64, uint8));
+ MOCK_METHOD2(UnusablePointerEncoding, void(uint64, uint8));
+ MOCK_METHOD2(RestoreInCIE, void(uint64, uint64));
+ MOCK_METHOD3(BadInstruction, void(uint64, CallFrameInfo::EntryKind, uint64));
+ MOCK_METHOD3(NoCFARule, void(uint64, CallFrameInfo::EntryKind, uint64));
+ MOCK_METHOD3(EmptyStateStack, void(uint64, CallFrameInfo::EntryKind, uint64));
+ MOCK_METHOD3(ClearingCFARule, void(uint64, CallFrameInfo::EntryKind, uint64));
+};
+
+struct CFIFixture {
+ enum { kCFARegister = CallFrameInfo::Handler::kCFARegister };
+
+ CFIFixture() {
+ // Default expectations for the data handler.
+ //
+ // - Leave Entry and End without expectations, as it's probably a
+ // good idea to set those explicitly in each test.
+ //
+ // - Expect the *Rule functions to not be called,
+ // so that each test can simply list the calls they expect.
+ //
+ // I gather I could use StrictMock for this, but the manual seems
+ // to suggest using that only as a last resort, and this isn't so
+ // bad.
+ EXPECT_CALL(handler, UndefinedRule(_, _)).Times(0);
+ EXPECT_CALL(handler, SameValueRule(_, _)).Times(0);
+ EXPECT_CALL(handler, OffsetRule(_, _, _, _)).Times(0);
+ EXPECT_CALL(handler, ValOffsetRule(_, _, _, _)).Times(0);
+ EXPECT_CALL(handler, RegisterRule(_, _, _)).Times(0);
+ EXPECT_CALL(handler, ExpressionRule(_, _, _)).Times(0);
+ EXPECT_CALL(handler, ValExpressionRule(_, _, _)).Times(0);
+ EXPECT_CALL(handler, PersonalityRoutine(_, _)).Times(0);
+ EXPECT_CALL(handler, LanguageSpecificDataArea(_, _)).Times(0);
+ EXPECT_CALL(handler, SignalHandler()).Times(0);
+
+ // Default expectations for the error/warning reporer.
+ EXPECT_CALL(reporter, Incomplete(_, _)).Times(0);
+ EXPECT_CALL(reporter, EarlyEHTerminator(_)).Times(0);
+ EXPECT_CALL(reporter, CIEPointerOutOfRange(_, _)).Times(0);
+ EXPECT_CALL(reporter, BadCIEId(_, _)).Times(0);
+ EXPECT_CALL(reporter, UnrecognizedVersion(_, _)).Times(0);
+ EXPECT_CALL(reporter, UnrecognizedAugmentation(_, _)).Times(0);
+ EXPECT_CALL(reporter, InvalidPointerEncoding(_, _)).Times(0);
+ EXPECT_CALL(reporter, UnusablePointerEncoding(_, _)).Times(0);
+ EXPECT_CALL(reporter, RestoreInCIE(_, _)).Times(0);
+ EXPECT_CALL(reporter, BadInstruction(_, _, _)).Times(0);
+ EXPECT_CALL(reporter, NoCFARule(_, _, _)).Times(0);
+ EXPECT_CALL(reporter, EmptyStateStack(_, _, _)).Times(0);
+ EXPECT_CALL(reporter, ClearingCFARule(_, _, _)).Times(0);
+ }
+
+ MockCallFrameInfoHandler handler;
+ MockCallFrameErrorReporter reporter;
+};
+
+class LulDwarfCFI : public CFIFixture, public Test {};
+
+TEST_F(LulDwarfCFI, EmptyRegion) {
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+ static const char data[1] = {42};
+
+ ByteReader reader(ENDIANNESS_BIG);
+ CallFrameInfo parser(data, 0, &reader, &handler, &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+TEST_F(LulDwarfCFI, IncompleteLength32) {
+ CFISection section(kBigEndian, 8);
+ section
+ // Not even long enough for an initial length.
+ .D16(0xa0f)
+ // Padding to keep valgrind happy. We subtract these off when we
+ // construct the parser.
+ .D16(0);
+
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+
+ EXPECT_CALL(reporter, Incomplete(_, CallFrameInfo::kUnknown))
+ .WillOnce(Return());
+
+ string contents;
+ ASSERT_TRUE(section.GetContents(&contents));
+
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(8);
+ CallFrameInfo parser(contents.data(), contents.size() - 2, &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+TEST_F(LulDwarfCFI, IncompleteLength64) {
+ CFISection section(kLittleEndian, 4);
+ section
+ // An incomplete 64-bit DWARF initial length.
+ .D32(0xffffffff)
+ .D32(0x71fbaec2)
+ // Padding to keep valgrind happy. We subtract these off when we
+ // construct the parser.
+ .D32(0);
+
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+
+ EXPECT_CALL(reporter, Incomplete(_, CallFrameInfo::kUnknown))
+ .WillOnce(Return());
+
+ string contents;
+ ASSERT_TRUE(section.GetContents(&contents));
+
+ ByteReader reader(ENDIANNESS_LITTLE);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size() - 4, &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+TEST_F(LulDwarfCFI, IncompleteId32) {
+ CFISection section(kBigEndian, 8);
+ section
+ .D32(3) // Initial length, not long enough for id
+ .D8(0xd7)
+ .D8(0xe5)
+ .D8(0xf1) // incomplete id
+ .CIEHeader(8727, 3983, 8889, 3, "")
+ .FinishEntry();
+
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+
+ EXPECT_CALL(reporter, Incomplete(_, CallFrameInfo::kUnknown))
+ .WillOnce(Return());
+
+ string contents;
+ ASSERT_TRUE(section.GetContents(&contents));
+
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(8);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+TEST_F(LulDwarfCFI, BadId32) {
+ CFISection section(kBigEndian, 8);
+ section
+ .D32(0x100) // Initial length
+ .D32(0xe802fade) // bogus ID
+ .Append(0x100 - 4, 0x42); // make the length true
+ section.CIEHeader(1672, 9872, 8529, 3, "").FinishEntry();
+
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+
+ EXPECT_CALL(reporter, CIEPointerOutOfRange(_, 0xe802fade)).WillOnce(Return());
+
+ string contents;
+ ASSERT_TRUE(section.GetContents(&contents));
+
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(8);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+// A lone CIE shouldn't cause any handler calls.
+TEST_F(LulDwarfCFI, SingleCIE) {
+ CFISection section(kLittleEndian, 4);
+ section.CIEHeader(0xffe799a8, 0x3398dcdd, 0x6e9683de, 3, "");
+ section.Append(10, lul::DW_CFA_nop);
+ section.FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("SingleCIE", section);
+
+ EXPECT_CALL(handler, Entry(_, _, _, _, _, _)).Times(0);
+ EXPECT_CALL(handler, End()).Times(0);
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_LITTLE);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+// One FDE, one CIE.
+TEST_F(LulDwarfCFI, OneFDE) {
+ CFISection section(kBigEndian, 4);
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(0x4be22f75, 0x2492236e, 0x6b6efb87, 3, "")
+ .FinishEntry()
+ .FDEHeader(cie, 0x7714740d, 0x3d5a10cd)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("OneFDE", section);
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0x7714740d, 0x3d5a10cd, 3, "", 0x6b6efb87))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+// Two FDEs share a CIE.
+TEST_F(LulDwarfCFI, TwoFDEsOneCIE) {
+ CFISection section(kBigEndian, 4);
+ Label cie;
+ section
+ // First FDE. readelf complains about this one because it makes
+ // a forward reference to its CIE.
+ .FDEHeader(cie, 0xa42744df, 0xa3b42121)
+ .FinishEntry()
+ // CIE.
+ .Mark(&cie)
+ .CIEHeader(0x04f7dc7b, 0x3d00c05f, 0xbd43cb59, 3, "")
+ .FinishEntry()
+ // Second FDE.
+ .FDEHeader(cie, 0x6057d391, 0x700f608d)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("TwoFDEsOneCIE", section);
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0xa42744df, 0xa3b42121, 3, "", 0xbd43cb59))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0x6057d391, 0x700f608d, 3, "", 0xbd43cb59))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+// Two FDEs, two CIEs.
+TEST_F(LulDwarfCFI, TwoFDEsTwoCIEs) {
+ CFISection section(kLittleEndian, 8);
+ Label cie1, cie2;
+ section
+ // First CIE.
+ .Mark(&cie1)
+ .CIEHeader(0x694d5d45, 0x4233221b, 0xbf45e65a, 3, "")
+ .FinishEntry()
+ // First FDE which cites second CIE. readelf complains about
+ // this one because it makes a forward reference to its CIE.
+ .FDEHeader(cie2, 0x778b27dfe5871f05ULL, 0x324ace3448070926ULL)
+ .FinishEntry()
+ // Second FDE, which cites first CIE.
+ .FDEHeader(cie1, 0xf6054ca18b10bf5fULL, 0x45fdb970d8bca342ULL)
+ .FinishEntry()
+ // Second CIE.
+ .Mark(&cie2)
+ .CIEHeader(0xfba3fad7, 0x6287e1fd, 0x61d2c581, 2, "")
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("TwoFDEsTwoCIEs", section);
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0x778b27dfe5871f05ULL, 0x324ace3448070926ULL,
+ 2, "", 0x61d2c581))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0xf6054ca18b10bf5fULL, 0x45fdb970d8bca342ULL,
+ 3, "", 0xbf45e65a))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_LITTLE);
+ reader.SetAddressSize(8);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+// An FDE whose CIE specifies a version we don't recognize.
+TEST_F(LulDwarfCFI, BadVersion) {
+ CFISection section(kBigEndian, 4);
+ Label cie1, cie2;
+ section.Mark(&cie1)
+ .CIEHeader(0xca878cf0, 0x7698ec04, 0x7b616f54, 0x52, "")
+ .FinishEntry()
+ // We should skip this entry, as its CIE specifies a version we
+ // don't recognize.
+ .FDEHeader(cie1, 0x08852292, 0x2204004a)
+ .FinishEntry()
+ // Despite the above, we should visit this entry.
+ .Mark(&cie2)
+ .CIEHeader(0x7c3ae7c9, 0xb9b9a512, 0x96cb3264, 3, "")
+ .FinishEntry()
+ .FDEHeader(cie2, 0x2094735a, 0x6e875501)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("BadVersion", section);
+
+ EXPECT_CALL(reporter, UnrecognizedVersion(_, 0x52)).WillOnce(Return());
+
+ {
+ InSequence s;
+ // We should see no mention of the first FDE, but we should get
+ // a call to Entry for the second.
+ EXPECT_CALL(handler, Entry(_, 0x2094735a, 0x6e875501, 3, "", 0x96cb3264))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+// An FDE whose CIE specifies an augmentation we don't recognize.
+TEST_F(LulDwarfCFI, BadAugmentation) {
+ CFISection section(kBigEndian, 4);
+ Label cie1, cie2;
+ section.Mark(&cie1)
+ .CIEHeader(0x4be22f75, 0x2492236e, 0x6b6efb87, 3, "spaniels!")
+ .FinishEntry()
+ // We should skip this entry, as its CIE specifies an
+ // augmentation we don't recognize.
+ .FDEHeader(cie1, 0x7714740d, 0x3d5a10cd)
+ .FinishEntry()
+ // Despite the above, we should visit this entry.
+ .Mark(&cie2)
+ .CIEHeader(0xf8bc4399, 0x8cf09931, 0xf2f519b2, 3, "")
+ .FinishEntry()
+ .FDEHeader(cie2, 0x7bf0fda0, 0xcbcd28d8)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("BadAugmentation", section);
+
+ EXPECT_CALL(reporter, UnrecognizedAugmentation(_, "spaniels!"))
+ .WillOnce(Return());
+
+ {
+ InSequence s;
+ // We should see no mention of the first FDE, but we should get
+ // a call to Entry for the second.
+ EXPECT_CALL(handler, Entry(_, 0x7bf0fda0, 0xcbcd28d8, 3, "", 0xf2f519b2))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_FALSE(parser.Start());
+}
+
+// The return address column field is a byte in CFI version 1
+// (DWARF2), but a ULEB128 value in version 3 (DWARF3).
+TEST_F(LulDwarfCFI, CIEVersion1ReturnColumn) {
+ CFISection section(kBigEndian, 4);
+ Label cie;
+ section
+ // CIE, using the version 1 format: return column is a ubyte.
+ .Mark(&cie)
+ // Use a value for the return column that is parsed differently
+ // as a ubyte and as a ULEB128.
+ .CIEHeader(0xbcdea24f, 0x5be28286, 0x9f, 1, "")
+ .FinishEntry()
+ // FDE, citing that CIE.
+ .FDEHeader(cie, 0xb8d347b5, 0x825e55dc)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("CIEVersion1ReturnColumn", section);
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0xb8d347b5, 0x825e55dc, 1, "", 0x9f))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+// The return address column field is a byte in CFI version 1
+// (DWARF2), but a ULEB128 value in version 3 (DWARF3).
+TEST_F(LulDwarfCFI, CIEVersion3ReturnColumn) {
+ CFISection section(kBigEndian, 4);
+ Label cie;
+ section
+ // CIE, using the version 3 format: return column is a ULEB128.
+ .Mark(&cie)
+ // Use a value for the return column that is parsed differently
+ // as a ubyte and as a ULEB128.
+ .CIEHeader(0x0ab4758d, 0xc010fdf7, 0x89, 3, "")
+ .FinishEntry()
+ // FDE, citing that CIE.
+ .FDEHeader(cie, 0x86763f2b, 0x2a66dc23)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("CIEVersion3ReturnColumn", section);
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler, Entry(_, 0x86763f2b, 0x2a66dc23, 3, "", 0x89))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ string contents;
+ EXPECT_TRUE(section.GetContents(&contents));
+ ByteReader reader(ENDIANNESS_BIG);
+ reader.SetAddressSize(4);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter);
+ EXPECT_TRUE(parser.Start());
+}
+
+struct CFIInsnFixture : public CFIFixture {
+ CFIInsnFixture() : CFIFixture() {
+ data_factor = 0xb6f;
+ return_register = 0x9be1ed9f;
+ version = 3;
+ cfa_base_register = 0x383a3aa;
+ cfa_offset = 0xf748;
+ }
+
+ // Prepare SECTION to receive FDE instructions.
+ //
+ // - Append a stock CIE header that establishes the fixture's
+ // code_factor, data_factor, return_register, version, and
+ // augmentation values.
+ // - Have the CIE set up a CFA rule using cfa_base_register and
+ // cfa_offset.
+ // - Append a stock FDE header, referring to the above CIE, for the
+ // fde_size bytes at fde_start. Choose fde_start and fde_size
+ // appropriately for the section's address size.
+ // - Set appropriate expectations on handler in sequence s for the
+ // frame description entry and the CIE's CFA rule.
+ //
+ // On return, SECTION is ready to have FDE instructions appended to
+ // it, and its FinishEntry member called.
+ void StockCIEAndFDE(CFISection* section) {
+ // Choose appropriate constants for our address size.
+ if (section->AddressSize() == 4) {
+ fde_start = 0xc628ecfbU;
+ fde_size = 0x5dee04a2;
+ code_factor = 0x60b;
+ } else {
+ assert(section->AddressSize() == 8);
+ fde_start = 0x0005c57ce7806bd3ULL;
+ fde_size = 0x2699521b5e333100ULL;
+ code_factor = 0x01008e32855274a8ULL;
+ }
+
+ // Create the CIE.
+ (*section)
+ .Mark(&cie_label)
+ .CIEHeader(code_factor, data_factor, return_register, version, "")
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(cfa_base_register)
+ .ULEB128(cfa_offset)
+ .FinishEntry();
+
+ // Create the FDE.
+ section->FDEHeader(cie_label, fde_start, fde_size);
+
+ // Expect an Entry call for the FDE and a ValOffsetRule call for the
+ // CIE's CFA rule.
+ EXPECT_CALL(handler,
+ Entry(_, fde_start, fde_size, version, "", return_register))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister,
+ cfa_base_register, cfa_offset))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ }
+
+ // Run the contents of SECTION through a CallFrameInfo parser,
+ // expecting parser.Start to return SUCCEEDS. Caller may optionally
+ // supply, via READER, its own ByteReader. If that's absent, a
+ // local one is used.
+ void ParseSection(CFISection* section, bool succeeds = true,
+ ByteReader* reader = nullptr) {
+ string contents;
+ EXPECT_TRUE(section->GetContents(&contents));
+ lul::Endianness endianness;
+ if (section->endianness() == kBigEndian)
+ endianness = ENDIANNESS_BIG;
+ else {
+ assert(section->endianness() == kLittleEndian);
+ endianness = ENDIANNESS_LITTLE;
+ }
+ ByteReader local_reader(endianness);
+ ByteReader* reader_to_use = reader ? reader : &local_reader;
+ reader_to_use->SetAddressSize(section->AddressSize());
+ CallFrameInfo parser(contents.data(), contents.size(), reader_to_use,
+ &handler, &reporter);
+ if (succeeds)
+ EXPECT_TRUE(parser.Start());
+ else
+ EXPECT_FALSE(parser.Start());
+ }
+
+ Label cie_label;
+ Sequence s;
+ uint64 code_factor;
+ int data_factor;
+ unsigned return_register;
+ unsigned version;
+ unsigned cfa_base_register;
+ int cfa_offset;
+ uint64 fde_start, fde_size;
+};
+
+class LulDwarfCFIInsn : public CFIInsnFixture, public Test {};
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_set_loc) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_set_loc)
+ .D32(0xb1ee3e7a)
+ // Use DW_CFA_def_cfa to force a handler call that we can use to
+ // check the effect of the DW_CFA_set_loc.
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x4defb431)
+ .ULEB128(0x6d17b0ee)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_set_loc", section);
+
+ EXPECT_CALL(handler,
+ ValOffsetRule(0xb1ee3e7a, kCFARegister, 0x4defb431, 0x6d17b0ee))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_advance_loc) {
+ CFISection section(kBigEndian, 8);
+ StockCIEAndFDE(&section);
+ section
+ .D8(lul::DW_CFA_advance_loc | 0x2a)
+ // Use DW_CFA_def_cfa to force a handler call that we can use to
+ // check the effect of the DW_CFA_advance_loc.
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x5bbb3715)
+ .ULEB128(0x0186c7bf)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_advance_loc", section);
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + 0x2a * code_factor,
+ kCFARegister, 0x5bbb3715, 0x0186c7bf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_advance_loc1) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_advance_loc1)
+ .D8(0xd8)
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x69d5696a)
+ .ULEB128(0x1eb7fc93)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_advance_loc1", section);
+
+ EXPECT_CALL(handler, ValOffsetRule((fde_start + 0xd8 * code_factor),
+ kCFARegister, 0x69d5696a, 0x1eb7fc93))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_advance_loc2) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_advance_loc2)
+ .D16(0x3adb)
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x3a368bed)
+ .ULEB128(0x3194ee37)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_advance_loc2", section);
+
+ EXPECT_CALL(handler, ValOffsetRule((fde_start + 0x3adb * code_factor),
+ kCFARegister, 0x3a368bed, 0x3194ee37))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_advance_loc4) {
+ CFISection section(kBigEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_advance_loc4)
+ .D32(0x15813c88)
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x135270c5)
+ .ULEB128(0x24bad7cb)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_advance_loc4", section);
+
+ EXPECT_CALL(handler, ValOffsetRule((fde_start + 0x15813c88ULL * code_factor),
+ kCFARegister, 0x135270c5, 0x24bad7cb))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_MIPS_advance_loc8) {
+ code_factor = 0x2d;
+ CFISection section(kBigEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_MIPS_advance_loc8)
+ .D64(0x3c4f3945b92c14ULL)
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0xe17ed602)
+ .ULEB128(0x3d162e7f)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_advance_loc8", section);
+
+ EXPECT_CALL(handler,
+ ValOffsetRule((fde_start + 0x3c4f3945b92c14ULL * code_factor),
+ kCFARegister, 0xe17ed602, 0x3d162e7f))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x4e363a85)
+ .ULEB128(0x815f9aa7)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("DW_CFA_def_cfa", section);
+
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x4e363a85, 0x815f9aa7))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_sf) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_sf)
+ .ULEB128(0x8ccb32b7)
+ .LEB128(0x9ea)
+ .D8(lul::DW_CFA_def_cfa_sf)
+ .ULEB128(0x9b40f5da)
+ .LEB128(-0x40a2)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister, 0x8ccb32b7,
+ 0x9ea * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister, 0x9b40f5da,
+ -0x40a2 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_register) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_register).ULEB128(0x3e7e9363).FinishEntry();
+
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x3e7e9363, cfa_offset))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+// DW_CFA_def_cfa_register should have no effect when applied to a
+// non-base/offset rule.
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_registerBadRule) {
+ ByteReader reader(ENDIANNESS_BIG);
+ CFISection section(kBigEndian, 4);
+ ImageSlice expr("needle in a haystack");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_expression)
+ .Block(expr)
+ .D8(lul::DW_CFA_def_cfa_register)
+ .ULEB128(0xf1b49e49)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, kCFARegister, expr))
+ .WillRepeatedly(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_offset) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_offset).ULEB128(0x1e8e3b9b).FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister, cfa_base_register,
+ 0x1e8e3b9b))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_offset_sf) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_offset_sf)
+ .LEB128(0x970)
+ .D8(lul::DW_CFA_def_cfa_offset_sf)
+ .LEB128(-0x2cd)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister, cfa_base_register,
+ 0x970 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, kCFARegister, cfa_base_register,
+ -0x2cd * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+// DW_CFA_def_cfa_offset should have no effect when applied to a
+// non-base/offset rule.
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_offsetBadRule) {
+ ByteReader reader(ENDIANNESS_BIG);
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ ImageSlice expr("six ways to Sunday");
+ section.D8(lul::DW_CFA_def_cfa_expression)
+ .Block(expr)
+ .D8(lul::DW_CFA_def_cfa_offset)
+ .ULEB128(0x1e8e3b9b)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, kCFARegister, expr))
+ .WillRepeatedly(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_def_cfa_expression) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 8);
+ ImageSlice expr("eating crow");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_def_cfa_expression).Block(expr).FinishEntry();
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, kCFARegister, expr))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_undefined) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_undefined).ULEB128(0x300ce45d).FinishEntry();
+
+ EXPECT_CALL(handler, UndefinedRule(fde_start, 0x300ce45d))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_same_value) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_same_value).ULEB128(0x3865a760).FinishEntry();
+
+ EXPECT_CALL(handler, SameValueRule(fde_start, 0x3865a760))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_offset) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset | 0x2c).ULEB128(0x9f6).FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x2c, kCFARegister, 0x9f6 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_offset_extended) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset_extended)
+ .ULEB128(0x402b)
+ .ULEB128(0xb48)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x402b, kCFARegister, 0xb48 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_offset_extended_sf) {
+ CFISection section(kBigEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset_extended_sf)
+ .ULEB128(0x997c23ee)
+ .LEB128(0x2d00)
+ .D8(lul::DW_CFA_offset_extended_sf)
+ .ULEB128(0x9519eb82)
+ .LEB128(-0xa77)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, OffsetRule(fde_start, 0x997c23ee, kCFARegister,
+ 0x2d00 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(fde_start, 0x9519eb82, kCFARegister,
+ -0xa77 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_val_offset) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_offset)
+ .ULEB128(0x623562fe)
+ .ULEB128(0x673)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0x623562fe, kCFARegister,
+ 0x673 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_val_offset_sf) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_offset_sf)
+ .ULEB128(0x6f4f)
+ .LEB128(0xaab)
+ .D8(lul::DW_CFA_val_offset_sf)
+ .ULEB128(0x2483)
+ .LEB128(-0x8a2)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0x6f4f, kCFARegister,
+ 0xaab * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0x2483, kCFARegister,
+ -0x8a2 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_register) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_register)
+ .ULEB128(0x278d18f9)
+ .ULEB128(0x1a684414)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, RegisterRule(fde_start, 0x278d18f9, 0x1a684414))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_expression) {
+ ByteReader reader(ENDIANNESS_BIG);
+ CFISection section(kBigEndian, 8);
+ StockCIEAndFDE(&section);
+ ImageSlice expr("plus ça change, plus c'est la même chose");
+ section.D8(lul::DW_CFA_expression)
+ .ULEB128(0xa1619fb2)
+ .Block(expr)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ExpressionRule(fde_start, 0xa1619fb2, expr))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_val_expression) {
+ ByteReader reader(ENDIANNESS_BIG);
+ CFISection section(kBigEndian, 4);
+ ImageSlice expr("he who has the gold makes the rules");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_expression)
+ .ULEB128(0xc5e4a9e3)
+ .Block(expr)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, 0xc5e4a9e3, expr))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_restore) {
+ CFISection section(kLittleEndian, 8);
+ code_factor = 0x01bd188a9b1fa083ULL;
+ data_factor = -0x1ac8;
+ return_register = 0x8c35b049;
+ version = 2;
+ fde_start = 0x2d70fe998298bbb1ULL;
+ fde_size = 0x46ccc2e63cf0b108ULL;
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(code_factor, data_factor, return_register, version, "")
+ // Provide a CFA rule, because register rules require them.
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x6ca1d50e)
+ .ULEB128(0x372e38e8)
+ // Provide an offset(N) rule for register 0x3c.
+ .D8(lul::DW_CFA_offset | 0x3c)
+ .ULEB128(0xb348)
+ .FinishEntry()
+ // In the FDE...
+ .FDEHeader(cie, fde_start, fde_size)
+ // At a second address, provide a new offset(N) rule for register 0x3c.
+ .D8(lul::DW_CFA_advance_loc | 0x13)
+ .D8(lul::DW_CFA_offset | 0x3c)
+ .ULEB128(0x9a50)
+ // At a third address, restore the original rule for register 0x3c.
+ .D8(lul::DW_CFA_advance_loc | 0x01)
+ .D8(lul::DW_CFA_restore | 0x3c)
+ .FinishEntry();
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler,
+ Entry(_, fde_start, fde_size, version, "", return_register))
+ .WillOnce(Return(true));
+ // CIE's CFA rule.
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x6ca1d50e, 0x372e38e8))
+ .WillOnce(Return(true));
+ // CIE's rule for register 0x3c.
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x3c, kCFARegister, 0xb348 * data_factor))
+ .WillOnce(Return(true));
+ // FDE's rule for register 0x3c.
+ EXPECT_CALL(handler, OffsetRule(fde_start + 0x13 * code_factor, 0x3c,
+ kCFARegister, 0x9a50 * data_factor))
+ .WillOnce(Return(true));
+ // Restore CIE's rule for register 0x3c.
+ EXPECT_CALL(handler, OffsetRule(fde_start + (0x13 + 0x01) * code_factor,
+ 0x3c, kCFARegister, 0xb348 * data_factor))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_restoreNoRule) {
+ CFISection section(kBigEndian, 4);
+ code_factor = 0x005f78143c1c3b82ULL;
+ data_factor = 0x25d0;
+ return_register = 0xe8;
+ version = 1;
+ fde_start = 0x4062e30f;
+ fde_size = 0x5302a389;
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(code_factor, data_factor, return_register, version, "")
+ // Provide a CFA rule, because register rules require them.
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x470aa334)
+ .ULEB128(0x099ef127)
+ .FinishEntry()
+ // In the FDE...
+ .FDEHeader(cie, fde_start, fde_size)
+ // At a second address, provide an offset(N) rule for register 0x2c.
+ .D8(lul::DW_CFA_advance_loc | 0x7)
+ .D8(lul::DW_CFA_offset | 0x2c)
+ .ULEB128(0x1f47)
+ // At a third address, restore the (missing) CIE rule for register 0x2c.
+ .D8(lul::DW_CFA_advance_loc | 0xb)
+ .D8(lul::DW_CFA_restore | 0x2c)
+ .FinishEntry();
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler,
+ Entry(_, fde_start, fde_size, version, "", return_register))
+ .WillOnce(Return(true));
+ // CIE's CFA rule.
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x470aa334, 0x099ef127))
+ .WillOnce(Return(true));
+ // FDE's rule for register 0x2c.
+ EXPECT_CALL(handler, OffsetRule(fde_start + 0x7 * code_factor, 0x2c,
+ kCFARegister, 0x1f47 * data_factor))
+ .WillOnce(Return(true));
+ // Restore CIE's (missing) rule for register 0x2c.
+ EXPECT_CALL(handler,
+ SameValueRule(fde_start + (0x7 + 0xb) * code_factor, 0x2c))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_restore_extended) {
+ CFISection section(kBigEndian, 4);
+ code_factor = 0x126e;
+ data_factor = -0xd8b;
+ return_register = 0x77711787;
+ version = 3;
+ fde_start = 0x01f55a45;
+ fde_size = 0x452adb80;
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(code_factor, data_factor, return_register, version, "",
+ true /* dwarf64 */)
+ // Provide a CFA rule, because register rules require them.
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x56fa0edd)
+ .ULEB128(0x097f78a5)
+ // Provide an offset(N) rule for register 0x0f9b8a1c.
+ .D8(lul::DW_CFA_offset_extended)
+ .ULEB128(0x0f9b8a1c)
+ .ULEB128(0xc979)
+ .FinishEntry()
+ // In the FDE...
+ .FDEHeader(cie, fde_start, fde_size)
+ // At a second address, provide a new offset(N) rule for reg 0x0f9b8a1c.
+ .D8(lul::DW_CFA_advance_loc | 0x3)
+ .D8(lul::DW_CFA_offset_extended)
+ .ULEB128(0x0f9b8a1c)
+ .ULEB128(0x3b7b)
+ // At a third address, restore the original rule for register 0x0f9b8a1c.
+ .D8(lul::DW_CFA_advance_loc | 0x04)
+ .D8(lul::DW_CFA_restore_extended)
+ .ULEB128(0x0f9b8a1c)
+ .FinishEntry();
+
+ {
+ InSequence s;
+ EXPECT_CALL(handler,
+ Entry(_, fde_start, fde_size, version, "", return_register))
+ .WillOnce(Return(true));
+ // CIE's CFA rule.
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x56fa0edd, 0x097f78a5))
+ .WillOnce(Return(true));
+ // CIE's rule for register 0x0f9b8a1c.
+ EXPECT_CALL(handler, OffsetRule(fde_start, 0x0f9b8a1c, kCFARegister,
+ 0xc979 * data_factor))
+ .WillOnce(Return(true));
+ // FDE's rule for register 0x0f9b8a1c.
+ EXPECT_CALL(handler, OffsetRule(fde_start + 0x3 * code_factor, 0x0f9b8a1c,
+ kCFARegister, 0x3b7b * data_factor))
+ .WillOnce(Return(true));
+ // Restore CIE's rule for register 0x0f9b8a1c.
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start + (0x3 + 0x4) * code_factor, 0x0f9b8a1c,
+ kCFARegister, 0xc979 * data_factor))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_remember_and_restore_state) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+
+ // We create a state, save it, modify it, and then restore. We
+ // refer to the state that is overridden the restore as the
+ // "outgoing" state, and the restored state the "incoming" state.
+ //
+ // Register outgoing incoming expect
+ // 1 offset(N) no rule new "same value" rule
+ // 2 register(R) offset(N) report changed rule
+ // 3 offset(N) offset(M) report changed offset
+ // 4 offset(N) offset(N) no report
+ // 5 offset(N) no rule new "same value" rule
+ section
+ // Create the "incoming" state, which we will save and later restore.
+ .D8(lul::DW_CFA_offset | 2)
+ .ULEB128(0x9806)
+ .D8(lul::DW_CFA_offset | 3)
+ .ULEB128(0x995d)
+ .D8(lul::DW_CFA_offset | 4)
+ .ULEB128(0x7055)
+ .D8(lul::DW_CFA_remember_state)
+ // Advance to a new instruction; an implementation could legitimately
+ // ignore all but the final rule for a given register at a given address.
+ .D8(lul::DW_CFA_advance_loc | 1)
+ // Create the "outgoing" state, which we will discard.
+ .D8(lul::DW_CFA_offset | 1)
+ .ULEB128(0xea1a)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(2)
+ .ULEB128(0x1d2a3767)
+ .D8(lul::DW_CFA_offset | 3)
+ .ULEB128(0xdd29)
+ .D8(lul::DW_CFA_offset | 5)
+ .ULEB128(0xf1ce)
+ // At a third address, restore the incoming state.
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ uint64 addr = fde_start;
+
+ // Expect the incoming rules to be reported.
+ EXPECT_CALL(handler, OffsetRule(addr, 2, kCFARegister, 0x9806 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 3, kCFARegister, 0x995d * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 4, kCFARegister, 0x7055 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+
+ addr += code_factor;
+
+ // After the save, we establish the outgoing rule set.
+ EXPECT_CALL(handler, OffsetRule(addr, 1, kCFARegister, 0xea1a * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, RegisterRule(addr, 2, 0x1d2a3767))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 3, kCFARegister, 0xdd29 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 5, kCFARegister, 0xf1ce * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+
+ addr += code_factor;
+
+ // Finally, after the restore, expect to see the differences from
+ // the outgoing to the incoming rules reported.
+ EXPECT_CALL(handler, SameValueRule(addr, 1))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 2, kCFARegister, 0x9806 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(addr, 3, kCFARegister, 0x995d * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, SameValueRule(addr, 5))
+ .InSequence(s)
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+// Check that restoring a rule set reports changes to the CFA rule.
+TEST_F(LulDwarfCFIInsn, DW_CFA_remember_and_restore_stateCFA) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+
+ section.D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_def_cfa_offset)
+ .ULEB128(0x90481102)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + code_factor, kCFARegister,
+ cfa_base_register, 0x90481102))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + code_factor * 2, kCFARegister,
+ cfa_base_register, cfa_offset))
+ .InSequence(s)
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_nop) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_nop)
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x3fb8d4f1)
+ .ULEB128(0x078dc67b)
+ .D8(lul::DW_CFA_nop)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ ValOffsetRule(fde_start, kCFARegister, 0x3fb8d4f1, 0x078dc67b))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_GNU_window_save) {
+ CFISection section(kBigEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_GNU_window_save).FinishEntry();
+
+ // Don't include all the rules in any particular sequence.
+
+ // The caller's %o0-%o7 have become the callee's %i0-%i7. This is
+ // the GCC register numbering.
+ for (int i = 8; i < 16; i++)
+ EXPECT_CALL(handler, RegisterRule(fde_start, i, i + 16))
+ .WillOnce(Return(true));
+ // The caller's %l0-%l7 and %i0-%i7 have been saved at the top of
+ // its frame.
+ for (int i = 16; i < 32; i++)
+ EXPECT_CALL(handler, OffsetRule(fde_start, i, kCFARegister, (i - 16) * 4))
+ .WillOnce(Return(true));
+
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_GNU_args_size) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_GNU_args_size)
+ .ULEB128(0xeddfa520)
+ // Verify that we see this, meaning we parsed the above properly.
+ .D8(lul::DW_CFA_offset | 0x23)
+ .ULEB128(0x269)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x23, kCFARegister, 0x269 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIInsn, DW_CFA_GNU_negative_offset_extended) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_GNU_negative_offset_extended)
+ .ULEB128(0x430cc87a)
+ .ULEB128(0x613)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, OffsetRule(fde_start, 0x430cc87a, kCFARegister,
+ -0x613 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+// Three FDEs: skip the second
+TEST_F(LulDwarfCFIInsn, SkipFDE) {
+ CFISection section(kBigEndian, 4);
+ Label cie;
+ section
+ // CIE, used by all FDEs.
+ .Mark(&cie)
+ .CIEHeader(0x010269f2, 0x9177, 0xedca5849, 2, "")
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(0x42ed390b)
+ .ULEB128(0x98f43aad)
+ .FinishEntry()
+ // First FDE.
+ .FDEHeader(cie, 0xa870ebdd, 0x60f6aa4)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(0x3a860351)
+ .ULEB128(0x6c9a6bcf)
+ .FinishEntry()
+ // Second FDE.
+ .FDEHeader(cie, 0xc534f7c0, 0xf6552e9, true /* dwarf64 */)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(0x1b62c234)
+ .ULEB128(0x26586b18)
+ .FinishEntry()
+ // Third FDE.
+ .FDEHeader(cie, 0xf681cfc8, 0x7e4594e)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(0x26c53934)
+ .ULEB128(0x18eeb8a4)
+ .FinishEntry();
+
+ {
+ InSequence s;
+
+ // Process the first FDE.
+ EXPECT_CALL(handler, Entry(_, 0xa870ebdd, 0x60f6aa4, 2, "", 0xedca5849))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ ValOffsetRule(0xa870ebdd, kCFARegister, 0x42ed390b, 0x98f43aad))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, RegisterRule(0xa870ebdd, 0x3a860351, 0x6c9a6bcf))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ // Skip the second FDE.
+ EXPECT_CALL(handler, Entry(_, 0xc534f7c0, 0xf6552e9, 2, "", 0xedca5849))
+ .WillOnce(Return(false));
+
+ // Process the third FDE.
+ EXPECT_CALL(handler, Entry(_, 0xf681cfc8, 0x7e4594e, 2, "", 0xedca5849))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ ValOffsetRule(0xf681cfc8, kCFARegister, 0x42ed390b, 0x98f43aad))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, RegisterRule(0xf681cfc8, 0x26c53934, 0x18eeb8a4))
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+ }
+
+ ParseSection(&section);
+}
+
+// Quit processing in the middle of an entry's instructions.
+TEST_F(LulDwarfCFIInsn, QuitMidentry) {
+ CFISection section(kLittleEndian, 8);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_register)
+ .ULEB128(0xe0cf850d)
+ .ULEB128(0x15aab431)
+ .D8(lul::DW_CFA_expression)
+ .ULEB128(0x46750aa5)
+ .Block("meat")
+ .FinishEntry();
+
+ EXPECT_CALL(handler, RegisterRule(fde_start, 0xe0cf850d, 0x15aab431))
+ .InSequence(s)
+ .WillOnce(Return(false));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseSection(&section, false);
+}
+
+class LulDwarfCFIRestore : public CFIInsnFixture, public Test {};
+
+TEST_F(LulDwarfCFIRestore, RestoreUndefinedRuleUnchanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_undefined)
+ .ULEB128(0x0bac878e)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, UndefinedRule(fde_start, 0x0bac878e))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreUndefinedRuleChanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_undefined)
+ .ULEB128(0x7dedff5f)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_same_value)
+ .ULEB128(0x7dedff5f)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, UndefinedRule(fde_start, 0x7dedff5f))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, SameValueRule(fde_start + code_factor, 0x7dedff5f))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + 2 * code_factor, 0x7dedff5f))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreSameValueRuleUnchanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_same_value)
+ .ULEB128(0xadbc9b3a)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, SameValueRule(fde_start, 0xadbc9b3a))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreSameValueRuleChanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_same_value)
+ .ULEB128(0x3d90dcb5)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0x3d90dcb5)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, SameValueRule(fde_start, 0x3d90dcb5))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0x3d90dcb5))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, SameValueRule(fde_start + 2 * code_factor, 0x3d90dcb5))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreOffsetRuleUnchanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset | 0x14)
+ .ULEB128(0xb6f)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x14, kCFARegister, 0xb6f * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreOffsetRuleChanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset | 0x21)
+ .ULEB128(0xeb7)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0x21)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x21, kCFARegister, 0xeb7 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0x21))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(fde_start + 2 * code_factor, 0x21,
+ kCFARegister, 0xeb7 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreOffsetRuleChangedOffset) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_offset | 0x21)
+ .ULEB128(0x134)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_offset | 0x21)
+ .ULEB128(0xf4f)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler,
+ OffsetRule(fde_start, 0x21, kCFARegister, 0x134 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(fde_start + code_factor, 0x21, kCFARegister,
+ 0xf4f * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, OffsetRule(fde_start + 2 * code_factor, 0x21,
+ kCFARegister, 0x134 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValOffsetRuleUnchanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_offset)
+ .ULEB128(0x829caee6)
+ .ULEB128(0xe4c)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0x829caee6, kCFARegister,
+ 0xe4c * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValOffsetRuleChanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_offset)
+ .ULEB128(0xf17c36d6)
+ .ULEB128(0xeb7)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0xf17c36d6)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0xf17c36d6, kCFARegister,
+ 0xeb7 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0xf17c36d6))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + 2 * code_factor, 0xf17c36d6,
+ kCFARegister, 0xeb7 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValOffsetRuleChangedValOffset) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_offset)
+ .ULEB128(0x2cf0ab1b)
+ .ULEB128(0x562)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_val_offset)
+ .ULEB128(0x2cf0ab1b)
+ .ULEB128(0xe88)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValOffsetRule(fde_start, 0x2cf0ab1b, kCFARegister,
+ 0x562 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + code_factor, 0x2cf0ab1b,
+ kCFARegister, 0xe88 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(fde_start + 2 * code_factor, 0x2cf0ab1b,
+ kCFARegister, 0x562 * data_factor))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreRegisterRuleUnchanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_register)
+ .ULEB128(0x77514acc)
+ .ULEB128(0x464de4ce)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, RegisterRule(fde_start, 0x77514acc, 0x464de4ce))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreRegisterRuleChanged) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_register)
+ .ULEB128(0xe39acce5)
+ .ULEB128(0x095f1559)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0xe39acce5)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, RegisterRule(fde_start, 0xe39acce5, 0x095f1559))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0xe39acce5))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ RegisterRule(fde_start + 2 * code_factor, 0xe39acce5, 0x095f1559))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreRegisterRuleChangedRegister) {
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_register)
+ .ULEB128(0xd40e21b1)
+ .ULEB128(0x16607d6a)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(0xd40e21b1)
+ .ULEB128(0xbabb4742)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, RegisterRule(fde_start, 0xd40e21b1, 0x16607d6a))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ RegisterRule(fde_start + code_factor, 0xd40e21b1, 0xbabb4742))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ RegisterRule(fde_start + 2 * code_factor, 0xd40e21b1, 0x16607d6a))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreExpressionRuleUnchanged) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ ImageSlice dwarf("dwarf");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_expression)
+ .ULEB128(0x666ae152)
+ .Block("dwarf")
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ExpressionRule(fde_start, 0x666ae152, dwarf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreExpressionRuleChanged) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ ImageSlice elf("elf");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_expression)
+ .ULEB128(0xb5ca5c46)
+ .Block(elf)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0xb5ca5c46)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ExpressionRule(fde_start, 0xb5ca5c46, elf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0xb5ca5c46))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ ExpressionRule(fde_start + 2 * code_factor, 0xb5ca5c46, elf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreExpressionRuleChangedExpression) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ StockCIEAndFDE(&section);
+ ImageSlice smurf("smurf");
+ ImageSlice orc("orc");
+ section.D8(lul::DW_CFA_expression)
+ .ULEB128(0x500f5739)
+ .Block(smurf)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_expression)
+ .ULEB128(0x500f5739)
+ .Block(orc)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ExpressionRule(fde_start, 0x500f5739, smurf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ExpressionRule(fde_start + code_factor, 0x500f5739, orc))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ // Expectations are not wishes.
+ EXPECT_CALL(handler,
+ ExpressionRule(fde_start + 2 * code_factor, 0x500f5739, smurf))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValExpressionRuleUnchanged) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ ImageSlice hideous("hideous");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_expression)
+ .ULEB128(0x666ae152)
+ .Block(hideous)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, 0x666ae152, hideous))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValExpressionRuleChanged) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ ImageSlice revolting("revolting");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_expression)
+ .ULEB128(0xb5ca5c46)
+ .Block(revolting)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0xb5ca5c46)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("RestoreValExpressionRuleChanged", section);
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, 0xb5ca5c46, revolting))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(fde_start + code_factor, 0xb5ca5c46))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValExpressionRule(fde_start + 2 * code_factor,
+ 0xb5ca5c46, revolting))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+TEST_F(LulDwarfCFIRestore, RestoreValExpressionRuleChangedValExpression) {
+ ByteReader reader(ENDIANNESS_LITTLE);
+ CFISection section(kLittleEndian, 4);
+ ImageSlice repulsive("repulsive");
+ ImageSlice nauseous("nauseous");
+ StockCIEAndFDE(&section);
+ section.D8(lul::DW_CFA_val_expression)
+ .ULEB128(0x500f5739)
+ .Block(repulsive)
+ .D8(lul::DW_CFA_remember_state)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_val_expression)
+ .ULEB128(0x500f5739)
+ .Block(nauseous)
+ .D8(lul::DW_CFA_advance_loc | 1)
+ .D8(lul::DW_CFA_restore_state)
+ .FinishEntry();
+
+ PERHAPS_WRITE_DEBUG_FRAME_FILE("RestoreValExpressionRuleChangedValExpression",
+ section);
+
+ EXPECT_CALL(handler, ValExpressionRule(fde_start, 0x500f5739, repulsive))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler,
+ ValExpressionRule(fde_start + code_factor, 0x500f5739, nauseous))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ // Expectations are not wishes.
+ EXPECT_CALL(handler, ValExpressionRule(fde_start + 2 * code_factor,
+ 0x500f5739, repulsive))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).WillOnce(Return(true));
+
+ ParseSection(&section, true, &reader);
+}
+
+struct EHFrameFixture : public CFIInsnFixture {
+ EHFrameFixture() : CFIInsnFixture(), section(kBigEndian, 4, true) {
+ encoded_pointer_bases.cfi = 0x7f496cb2;
+ encoded_pointer_bases.text = 0x540f67b6;
+ encoded_pointer_bases.data = 0xe3eab768;
+ section.SetEncodedPointerBases(encoded_pointer_bases);
+ }
+ CFISection section;
+ CFISection::EncodedPointerBases encoded_pointer_bases;
+
+ // Parse CFIInsnFixture::ParseSection, but parse the section as
+ // .eh_frame data, supplying stock base addresses.
+ void ParseEHFrameSection(CFISection* section, bool succeeds = true) {
+ EXPECT_TRUE(section->ContainsEHFrame());
+ string contents;
+ EXPECT_TRUE(section->GetContents(&contents));
+ lul::Endianness endianness;
+ if (section->endianness() == kBigEndian)
+ endianness = ENDIANNESS_BIG;
+ else {
+ assert(section->endianness() == kLittleEndian);
+ endianness = ENDIANNESS_LITTLE;
+ }
+ ByteReader reader(endianness);
+ reader.SetAddressSize(section->AddressSize());
+ reader.SetCFIDataBase(encoded_pointer_bases.cfi, contents.data());
+ reader.SetTextBase(encoded_pointer_bases.text);
+ reader.SetDataBase(encoded_pointer_bases.data);
+ CallFrameInfo parser(contents.data(), contents.size(), &reader, &handler,
+ &reporter, true);
+ if (succeeds)
+ EXPECT_TRUE(parser.Start());
+ else
+ EXPECT_FALSE(parser.Start());
+ }
+};
+
+class LulDwarfEHFrame : public EHFrameFixture, public Test {};
+
+// A simple CIE, an FDE, and a terminator.
+TEST_F(LulDwarfEHFrame, Terminator) {
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(9968, 2466, 67, 1, "")
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(3772)
+ .ULEB128(1372)
+ .FinishEntry()
+ .FDEHeader(cie, 0x848037a1, 0x7b30475e)
+ .D8(lul::DW_CFA_set_loc)
+ .D32(0x17713850)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(5721)
+ .FinishEntry()
+ .D32(0) // Terminate the sequence.
+ // This FDE should be ignored.
+ .FDEHeader(cie, 0xf19629fe, 0x439fb09b)
+ .FinishEntry();
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.Terminator", section);
+
+ EXPECT_CALL(handler, Entry(_, 0x848037a1, 0x7b30475e, 1, "", 67))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(0x848037a1, kCFARegister, 3772, 1372))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(0x17713850, 5721))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+ EXPECT_CALL(reporter, EarlyEHTerminator(_)).InSequence(s).WillOnce(Return());
+
+ ParseEHFrameSection(&section);
+}
+
+// The parser should recognize the Linux Standards Base 'z' augmentations.
+TEST_F(LulDwarfEHFrame, SimpleFDE) {
+ lul::DwarfPointerEncoding lsda_encoding = lul::DwarfPointerEncoding(
+ lul::DW_EH_PE_indirect | lul::DW_EH_PE_datarel | lul::DW_EH_PE_sdata2);
+ lul::DwarfPointerEncoding fde_encoding =
+ lul::DwarfPointerEncoding(lul::DW_EH_PE_textrel | lul::DW_EH_PE_udata2);
+
+ section.SetPointerEncoding(fde_encoding);
+ section.SetEncodedPointerBases(encoded_pointer_bases);
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(4873, 7012, 100, 1, "zSLPR")
+ .ULEB128(7) // Augmentation data length
+ .D8(lsda_encoding) // LSDA pointer format
+ .D8(lul::DW_EH_PE_pcrel) // personality pointer format
+ .EncodedPointer(0x97baa00, lul::DW_EH_PE_pcrel) // and value
+ .D8(fde_encoding) // FDE pointer format
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(6706)
+ .ULEB128(31)
+ .FinishEntry()
+ .FDEHeader(cie, 0x540f6b56, 0xf686)
+ .ULEB128(2) // Augmentation data length
+ .EncodedPointer(0xe3eab475, lsda_encoding) // LSDA pointer, signed
+ .D8(lul::DW_CFA_set_loc)
+ .EncodedPointer(0x540fa4ce, fde_encoding)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(0x675e)
+ .FinishEntry()
+ .D32(0); // terminator
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.SimpleFDE", section);
+
+ EXPECT_CALL(handler, Entry(_, 0x540f6b56, 0xf686, 1, "zSLPR", 100))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, PersonalityRoutine(0x97baa00, false))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, LanguageSpecificDataArea(0xe3eab475, true))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, SignalHandler()).InSequence(s).WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(0x540f6b56, kCFARegister, 6706, 31))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(0x540fa4ce, 0x675e))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+// Check that we can handle an empty 'z' augmentation.
+TEST_F(LulDwarfEHFrame, EmptyZ) {
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(5955, 5805, 228, 1, "z")
+ .ULEB128(0) // Augmentation data length
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(3629)
+ .ULEB128(247)
+ .FinishEntry()
+ .FDEHeader(cie, 0xda007738, 0xfb55c641)
+ .ULEB128(0) // Augmentation data length
+ .D8(lul::DW_CFA_advance_loc1)
+ .D8(11)
+ .D8(lul::DW_CFA_undefined)
+ .ULEB128(3769)
+ .FinishEntry();
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.EmptyZ", section);
+
+ EXPECT_CALL(handler, Entry(_, 0xda007738, 0xfb55c641, 1, "z", 228))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, ValOffsetRule(0xda007738, kCFARegister, 3629, 247))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, UndefinedRule(0xda007738 + 11 * 5955, 3769))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+// Check that we recognize bad 'z' augmentation characters.
+TEST_F(LulDwarfEHFrame, BadZ) {
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(6937, 1045, 142, 1, "zQ")
+ .ULEB128(0) // Augmentation data length
+ .D8(lul::DW_CFA_def_cfa)
+ .ULEB128(9006)
+ .ULEB128(7725)
+ .FinishEntry()
+ .FDEHeader(cie, 0x1293efa8, 0x236f53f2)
+ .ULEB128(0) // Augmentation data length
+ .D8(lul::DW_CFA_advance_loc | 12)
+ .D8(lul::DW_CFA_register)
+ .ULEB128(5667)
+ .ULEB128(3462)
+ .FinishEntry();
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.BadZ", section);
+
+ EXPECT_CALL(reporter, UnrecognizedAugmentation(_, "zQ")).WillOnce(Return());
+
+ ParseEHFrameSection(&section, false);
+}
+
+TEST_F(LulDwarfEHFrame, zL) {
+ Label cie;
+ lul::DwarfPointerEncoding lsda_encoding =
+ lul::DwarfPointerEncoding(lul::DW_EH_PE_funcrel | lul::DW_EH_PE_udata2);
+ section.Mark(&cie)
+ .CIEHeader(9285, 9959, 54, 1, "zL")
+ .ULEB128(1) // Augmentation data length
+ .D8(lsda_encoding) // encoding for LSDA pointer in FDE
+
+ .FinishEntry()
+ .FDEHeader(cie, 0xd40091aa, 0x9aa6e746)
+ .ULEB128(2) // Augmentation data length
+ .EncodedPointer(0xd40099cd, lsda_encoding) // LSDA pointer
+ .FinishEntry()
+ .D32(0); // terminator
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.zL", section);
+
+ EXPECT_CALL(handler, Entry(_, 0xd40091aa, 0x9aa6e746, 1, "zL", 54))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, LanguageSpecificDataArea(0xd40099cd, false))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+TEST_F(LulDwarfEHFrame, zP) {
+ Label cie;
+ lul::DwarfPointerEncoding personality_encoding =
+ lul::DwarfPointerEncoding(lul::DW_EH_PE_datarel | lul::DW_EH_PE_udata2);
+ section.Mark(&cie)
+ .CIEHeader(1097, 6313, 17, 1, "zP")
+ .ULEB128(3) // Augmentation data length
+ .D8(personality_encoding) // encoding for personality routine
+ .EncodedPointer(0xe3eaccac, personality_encoding) // value
+ .FinishEntry()
+ .FDEHeader(cie, 0x0c8350c9, 0xbef11087)
+ .ULEB128(0) // Augmentation data length
+ .FinishEntry()
+ .D32(0); // terminator
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.zP", section);
+
+ EXPECT_CALL(handler, Entry(_, 0x0c8350c9, 0xbef11087, 1, "zP", 17))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, PersonalityRoutine(0xe3eaccac, false))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+TEST_F(LulDwarfEHFrame, zR) {
+ Label cie;
+ lul::DwarfPointerEncoding pointer_encoding =
+ lul::DwarfPointerEncoding(lul::DW_EH_PE_textrel | lul::DW_EH_PE_sdata2);
+ section.SetPointerEncoding(pointer_encoding);
+ section.Mark(&cie)
+ .CIEHeader(8011, 5496, 75, 1, "zR")
+ .ULEB128(1) // Augmentation data length
+ .D8(pointer_encoding) // encoding for FDE addresses
+ .FinishEntry()
+ .FDEHeader(cie, 0x540f9431, 0xbd0)
+ .ULEB128(0) // Augmentation data length
+ .FinishEntry()
+ .D32(0); // terminator
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.zR", section);
+
+ EXPECT_CALL(handler, Entry(_, 0x540f9431, 0xbd0, 1, "zR", 75))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+TEST_F(LulDwarfEHFrame, zS) {
+ Label cie;
+ section.Mark(&cie)
+ .CIEHeader(9217, 7694, 57, 1, "zS")
+ .ULEB128(0) // Augmentation data length
+ .FinishEntry()
+ .FDEHeader(cie, 0xd40091aa, 0x9aa6e746)
+ .ULEB128(0) // Augmentation data length
+ .FinishEntry()
+ .D32(0); // terminator
+
+ PERHAPS_WRITE_EH_FRAME_FILE("EHFrame.zS", section);
+
+ EXPECT_CALL(handler, Entry(_, 0xd40091aa, 0x9aa6e746, 1, "zS", 57))
+ .InSequence(s)
+ .WillOnce(Return(true));
+ EXPECT_CALL(handler, SignalHandler()).InSequence(s).WillOnce(Return(true));
+ EXPECT_CALL(handler, End()).InSequence(s).WillOnce(Return(true));
+
+ ParseEHFrameSection(&section);
+}
+
+// These tests require manual inspection of the test output.
+struct CFIReporterFixture {
+ CFIReporterFixture()
+ : reporter(gtest_logging_sink_for_LulTestDwarf, "test file name",
+ "test section name") {}
+ CallFrameInfo::Reporter reporter;
+};
+
+class LulDwarfCFIReporter : public CFIReporterFixture, public Test {};
+
+TEST_F(LulDwarfCFIReporter, Incomplete) {
+ reporter.Incomplete(0x0102030405060708ULL, CallFrameInfo::kUnknown);
+}
+
+TEST_F(LulDwarfCFIReporter, EarlyEHTerminator) {
+ reporter.EarlyEHTerminator(0x0102030405060708ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, CIEPointerOutOfRange) {
+ reporter.CIEPointerOutOfRange(0x0123456789abcdefULL, 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, BadCIEId) {
+ reporter.BadCIEId(0x0123456789abcdefULL, 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, UnrecognizedVersion) {
+ reporter.UnrecognizedVersion(0x0123456789abcdefULL, 43);
+}
+
+TEST_F(LulDwarfCFIReporter, UnrecognizedAugmentation) {
+ reporter.UnrecognizedAugmentation(0x0123456789abcdefULL, "poodles");
+}
+
+TEST_F(LulDwarfCFIReporter, InvalidPointerEncoding) {
+ reporter.InvalidPointerEncoding(0x0123456789abcdefULL, 0x42);
+}
+
+TEST_F(LulDwarfCFIReporter, UnusablePointerEncoding) {
+ reporter.UnusablePointerEncoding(0x0123456789abcdefULL, 0x42);
+}
+
+TEST_F(LulDwarfCFIReporter, RestoreInCIE) {
+ reporter.RestoreInCIE(0x0123456789abcdefULL, 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, BadInstruction) {
+ reporter.BadInstruction(0x0123456789abcdefULL, CallFrameInfo::kFDE,
+ 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, NoCFARule) {
+ reporter.NoCFARule(0x0123456789abcdefULL, CallFrameInfo::kCIE,
+ 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, EmptyStateStack) {
+ reporter.EmptyStateStack(0x0123456789abcdefULL, CallFrameInfo::kTerminator,
+ 0xfedcba9876543210ULL);
+}
+
+TEST_F(LulDwarfCFIReporter, ClearingCFARule) {
+ reporter.ClearingCFARule(0x0123456789abcdefULL, CallFrameInfo::kFDE,
+ 0xfedcba9876543210ULL);
+}
+class LulDwarfExpr : public Test {};
+
+class MockSummariser : public Summariser {
+ public:
+ MockSummariser() : Summariser(nullptr, 0, nullptr) {}
+ MOCK_METHOD2(Entry, void(uintptr_t, uintptr_t));
+ MOCK_METHOD0(End, void());
+ MOCK_METHOD5(Rule, void(uintptr_t, int, LExprHow, int16_t, int64_t));
+ MOCK_METHOD1(AddPfxInstr, uint32_t(PfxInstr));
+};
+
+TEST_F(LulDwarfExpr, SimpleTransliteration) {
+ MockSummariser summ;
+ ByteReader reader(ENDIANNESS_LITTLE);
+
+ CFISection section(kLittleEndian, 8);
+ section.D8(DW_OP_lit0)
+ .D8(DW_OP_lit31)
+ .D8(DW_OP_breg0 + 17)
+ .LEB128(-1234)
+ .D8(DW_OP_const4s)
+ .D32(0xFEDC9876)
+ .D8(DW_OP_deref)
+ .D8(DW_OP_and)
+ .D8(DW_OP_plus)
+ .D8(DW_OP_minus)
+ .D8(DW_OP_shl)
+ .D8(DW_OP_ge);
+ string expr;
+ bool ok = section.GetContents(&expr);
+ EXPECT_TRUE(ok);
+
+ {
+ InSequence s;
+ // required start marker
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Start, 0)));
+ // DW_OP_lit0
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_SImm32, 0)));
+ // DW_OP_lit31
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_SImm32, 31)));
+ // DW_OP_breg17 -1234
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_DwReg, 17)));
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_SImm32, -1234)));
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Add)));
+ // DW_OP_const4s 0xFEDC9876
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_SImm32, 0xFEDC9876)));
+ // DW_OP_deref
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Deref)));
+ // DW_OP_and
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_And)));
+ // DW_OP_plus
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Add)));
+ // DW_OP_minus
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Sub)));
+ // DW_OP_shl
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Shl)));
+ // DW_OP_ge
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_CmpGES)));
+ // required end marker
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_End)));
+ }
+
+ int32_t ix =
+ parseDwarfExpr(&summ, &reader, ImageSlice(expr), false, false, false);
+ EXPECT_TRUE(ix >= 0);
+}
+
+TEST_F(LulDwarfExpr, UnknownOpcode) {
+ MockSummariser summ;
+ ByteReader reader(ENDIANNESS_LITTLE);
+
+ CFISection section(kLittleEndian, 8);
+ section.D8(DW_OP_lo_user - 1);
+ string expr;
+ bool ok = section.GetContents(&expr);
+ EXPECT_TRUE(ok);
+
+ {
+ InSequence s;
+ // required start marker
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Start, 0)));
+ }
+
+ int32_t ix =
+ parseDwarfExpr(&summ, &reader, ImageSlice(expr), false, false, false);
+ EXPECT_TRUE(ix == -1);
+}
+
+TEST_F(LulDwarfExpr, ExpressionOverrun) {
+ MockSummariser summ;
+ ByteReader reader(ENDIANNESS_LITTLE);
+
+ CFISection section(kLittleEndian, 8);
+ section.D8(DW_OP_const4s).D8(0x12).D8(0x34).D8(0x56);
+ string expr;
+ bool ok = section.GetContents(&expr);
+ EXPECT_TRUE(ok);
+
+ {
+ InSequence s;
+ // required start marker
+ EXPECT_CALL(summ, AddPfxInstr(PfxInstr(PX_Start, 0)));
+ // DW_OP_const4s followed by 3 (a.k.a. not enough) bytes
+ // We expect PfxInstr(PX_Simm32, not-known-for-sure-32-bit-immediate)
+ // Hence must use _ as the argument.
+ EXPECT_CALL(summ, AddPfxInstr(_));
+ }
+
+ int32_t ix =
+ parseDwarfExpr(&summ, &reader, ImageSlice(expr), false, false, false);
+ EXPECT_TRUE(ix == -1);
+}
+
+// We'll need to mention specific Dwarf registers in the EvaluatePfxExpr tests,
+// and those names are arch-specific, so a bit of macro magic is helpful.
+#if defined(GP_ARCH_arm)
+# define TESTED_REG_STRUCT_NAME r11
+# define TESTED_REG_DWARF_NAME DW_REG_ARM_R11
+#elif defined(GP_ARCH_arm64)
+# define TESTED_REG_STRUCT_NAME x29
+# define TESTED_REG_DWARF_NAME DW_REG_AARCH64_X29
+#elif defined(GP_ARCH_amd64) || defined(GP_ARCH_x86)
+# define TESTED_REG_STRUCT_NAME xbp
+# define TESTED_REG_DWARF_NAME DW_REG_INTEL_XBP
+#else
+# error "Unknown plat"
+#endif
+
+struct EvaluatePfxExprFixture {
+ // Creates:
+ // initial stack, AVMA 0x12345678, at offset 4 bytes = 0xdeadbeef
+ // initial regs, with XBP = 0x14141356
+ // initial CFA = 0x5432ABCD
+ EvaluatePfxExprFixture() {
+ // The test stack.
+ si.mStartAvma = 0x12345678;
+ si.mLen = 0;
+#define XX(_byte) \
+ do { \
+ si.mContents[si.mLen++] = (_byte); \
+ } while (0)
+ XX(0x55);
+ XX(0x55);
+ XX(0x55);
+ XX(0x55);
+ if (sizeof(void*) == 8) {
+ // le64
+ XX(0xEF);
+ XX(0xBE);
+ XX(0xAD);
+ XX(0xDE);
+ XX(0);
+ XX(0);
+ XX(0);
+ XX(0);
+ } else {
+ // le32
+ XX(0xEF);
+ XX(0xBE);
+ XX(0xAD);
+ XX(0xDE);
+ }
+ XX(0xAA);
+ XX(0xAA);
+ XX(0xAA);
+ XX(0xAA);
+#undef XX
+ // The initial CFA.
+ initialCFA = TaggedUWord(0x5432ABCD);
+ // The initial register state.
+ memset(&regs, 0, sizeof(regs));
+ regs.TESTED_REG_STRUCT_NAME = TaggedUWord(0x14141356);
+ }
+
+ StackImage si;
+ TaggedUWord initialCFA;
+ UnwindRegs regs;
+};
+
+class LulDwarfEvaluatePfxExpr : public EvaluatePfxExprFixture, public Test {};
+
+TEST_F(LulDwarfEvaluatePfxExpr, NormalEvaluation) {
+ vector<PfxInstr> instrs;
+ // Put some junk at the start of the insn sequence.
+ instrs.push_back(PfxInstr(PX_End));
+ instrs.push_back(PfxInstr(PX_End));
+
+ // Now the real sequence
+ // stack is empty
+ instrs.push_back(PfxInstr(PX_Start, 1));
+ // 0x5432ABCD
+ instrs.push_back(PfxInstr(PX_SImm32, 0x31415927));
+ // 0x5432ABCD 0x31415927
+ instrs.push_back(PfxInstr(PX_DwReg, TESTED_REG_DWARF_NAME));
+ // 0x5432ABCD 0x31415927 0x14141356
+ instrs.push_back(PfxInstr(PX_SImm32, 42));
+ // 0x5432ABCD 0x31415927 0x14141356 42
+ instrs.push_back(PfxInstr(PX_Sub));
+ // 0x5432ABCD 0x31415927 0x1414132c
+ instrs.push_back(PfxInstr(PX_Add));
+ // 0x5432ABCD 0x45556c53
+ instrs.push_back(PfxInstr(PX_SImm32, si.mStartAvma + 4));
+ // 0x5432ABCD 0x45556c53 0x1234567c
+ instrs.push_back(PfxInstr(PX_Deref));
+ // 0x5432ABCD 0x45556c53 0xdeadbeef
+ instrs.push_back(PfxInstr(PX_SImm32, 0xFE01DC23));
+ // 0x5432ABCD 0x45556c53 0xdeadbeef 0xFE01DC23
+ instrs.push_back(PfxInstr(PX_And));
+ // 0x5432ABCD 0x45556c53 0xde019c23
+ instrs.push_back(PfxInstr(PX_SImm32, 7));
+ // 0x5432ABCD 0x45556c53 0xde019c23 7
+ instrs.push_back(PfxInstr(PX_Shl));
+ // 0x5432ABCD 0x45556c53 0x6f00ce1180
+ instrs.push_back(PfxInstr(PX_SImm32, 0x7fffffff));
+ // 0x5432ABCD 0x45556c53 0x6f00ce1180 7fffffff
+ instrs.push_back(PfxInstr(PX_And));
+ // 0x5432ABCD 0x45556c53 0x00ce1180
+ instrs.push_back(PfxInstr(PX_Add));
+ // 0x5432ABCD 0x46237dd3
+ instrs.push_back(PfxInstr(PX_Sub));
+ // 0xe0f2dfa
+
+ instrs.push_back(PfxInstr(PX_End));
+
+ TaggedUWord res = EvaluatePfxExpr(2 /*offset of start insn*/, &regs,
+ initialCFA, &si, instrs);
+ EXPECT_TRUE(res.Valid());
+ EXPECT_TRUE(res.Value() == 0xe0f2dfa);
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, EmptySequence) {
+ vector<PfxInstr> instrs;
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_FALSE(res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, BogusStartPoint) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_SImm32, 42));
+ instrs.push_back(PfxInstr(PX_SImm32, 24));
+ instrs.push_back(PfxInstr(PX_SImm32, 4224));
+ TaggedUWord res = EvaluatePfxExpr(1, &regs, initialCFA, &si, instrs);
+ EXPECT_FALSE(res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, MissingEndMarker) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ instrs.push_back(PfxInstr(PX_SImm32, 24));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_FALSE(res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, StackUnderflow) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ instrs.push_back(PfxInstr(PX_End));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_FALSE(res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, StackNoUnderflow) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 1 /*push the initial CFA*/));
+ instrs.push_back(PfxInstr(PX_End));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_TRUE(res.Valid());
+ EXPECT_TRUE(res == initialCFA);
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, StackOverflow) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ for (int i = 0; i < 10 + 1; i++) {
+ instrs.push_back(PfxInstr(PX_SImm32, i + 100));
+ }
+ instrs.push_back(PfxInstr(PX_End));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_FALSE(res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, StackNoOverflow) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ for (int i = 0; i < 10 + 0; i++) {
+ instrs.push_back(PfxInstr(PX_SImm32, i + 100));
+ }
+ instrs.push_back(PfxInstr(PX_End));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_TRUE(res.Valid());
+ EXPECT_TRUE(res == TaggedUWord(109));
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, OutOfRangeShl) {
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ instrs.push_back(PfxInstr(PX_SImm32, 1234));
+ instrs.push_back(PfxInstr(PX_SImm32, 5678));
+ instrs.push_back(PfxInstr(PX_Shl));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_TRUE(!res.Valid());
+}
+
+TEST_F(LulDwarfEvaluatePfxExpr, TestCmpGES) {
+ const int32_t argsL[6] = {0, 0, 1, -2, -1, -2};
+ const int32_t argsR[6] = {0, 1, 0, -2, -2, -1};
+ // expecting: t f t t t f = 101110 = 0x2E
+ vector<PfxInstr> instrs;
+ instrs.push_back(PfxInstr(PX_Start, 0));
+ // The "running total"
+ instrs.push_back(PfxInstr(PX_SImm32, 0));
+ for (unsigned int i = 0; i < sizeof(argsL) / sizeof(argsL[0]); i++) {
+ // Shift the "running total" at the bottom of the stack left by one bit
+ instrs.push_back(PfxInstr(PX_SImm32, 1));
+ instrs.push_back(PfxInstr(PX_Shl));
+ // Push both test args and do the comparison
+ instrs.push_back(PfxInstr(PX_SImm32, argsL[i]));
+ instrs.push_back(PfxInstr(PX_SImm32, argsR[i]));
+ instrs.push_back(PfxInstr(PX_CmpGES));
+ // Or the result into the running total
+ instrs.push_back(PfxInstr(PX_Or));
+ }
+ instrs.push_back(PfxInstr(PX_End));
+ TaggedUWord res = EvaluatePfxExpr(0, &regs, initialCFA, &si, instrs);
+ EXPECT_TRUE(res.Valid());
+ EXPECT_TRUE(res == TaggedUWord(0x2E));
+}
+
+} // namespace lul
diff --git a/tools/profiler/tests/gtest/LulTestInfrastructure.cpp b/tools/profiler/tests/gtest/LulTestInfrastructure.cpp
new file mode 100644
index 0000000000..6d49557e9c
--- /dev/null
+++ b/tools/profiler/tests/gtest/LulTestInfrastructure.cpp
@@ -0,0 +1,498 @@
+// Copyright (c) 2010, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Original author: Jim Blandy <jimb@mozilla.com> <jimb@red-bean.com>
+
+// Derived from:
+// test_assembler.cc: Implementation of google_breakpad::TestAssembler.
+// See test_assembler.h for details.
+
+// Derived from:
+// cfi_assembler.cc: Implementation of google_breakpad::CFISection class.
+// See cfi_assembler.h for details.
+
+#include "LulTestInfrastructure.h"
+
+#include "LulDwarfInt.h"
+
+#include <cassert>
+
+namespace lul_test {
+namespace test_assembler {
+
+using std::back_insert_iterator;
+
+Label::Label() : value_(new Binding()) {}
+Label::Label(uint64_t value) : value_(new Binding(value)) {}
+Label::Label(const Label& label) {
+ value_ = label.value_;
+ value_->Acquire();
+}
+Label::~Label() {
+ if (value_->Release()) delete value_;
+}
+
+Label& Label::operator=(uint64_t value) {
+ value_->Set(NULL, value);
+ return *this;
+}
+
+Label& Label::operator=(const Label& label) {
+ value_->Set(label.value_, 0);
+ return *this;
+}
+
+Label Label::operator+(uint64_t addend) const {
+ Label l;
+ l.value_->Set(this->value_, addend);
+ return l;
+}
+
+Label Label::operator-(uint64_t subtrahend) const {
+ Label l;
+ l.value_->Set(this->value_, -subtrahend);
+ return l;
+}
+
+// When NDEBUG is #defined, assert doesn't evaluate its argument. This
+// means you can't simply use assert to check the return value of a
+// function with necessary side effects.
+//
+// ALWAYS_EVALUATE_AND_ASSERT(x) evaluates x regardless of whether
+// NDEBUG is #defined; when NDEBUG is not #defined, it further asserts
+// that x is true.
+#ifdef NDEBUG
+# define ALWAYS_EVALUATE_AND_ASSERT(x) x
+#else
+# define ALWAYS_EVALUATE_AND_ASSERT(x) assert(x)
+#endif
+
+uint64_t Label::operator-(const Label& label) const {
+ uint64_t offset;
+ ALWAYS_EVALUATE_AND_ASSERT(IsKnownOffsetFrom(label, &offset));
+ return offset;
+}
+
+bool Label::IsKnownConstant(uint64_t* value_p) const {
+ Binding* base;
+ uint64_t addend;
+ value_->Get(&base, &addend);
+ if (base != NULL) return false;
+ if (value_p) *value_p = addend;
+ return true;
+}
+
+bool Label::IsKnownOffsetFrom(const Label& label, uint64_t* offset_p) const {
+ Binding *label_base, *this_base;
+ uint64_t label_addend, this_addend;
+ label.value_->Get(&label_base, &label_addend);
+ value_->Get(&this_base, &this_addend);
+ // If this and label are related, Get will find their final
+ // common ancestor, regardless of how indirect the relation is. This
+ // comparison also handles the constant vs. constant case.
+ if (this_base != label_base) return false;
+ if (offset_p) *offset_p = this_addend - label_addend;
+ return true;
+}
+
+Label::Binding::Binding() : base_(this), addend_(), reference_count_(1) {}
+
+Label::Binding::Binding(uint64_t addend)
+ : base_(NULL), addend_(addend), reference_count_(1) {}
+
+Label::Binding::~Binding() {
+ assert(reference_count_ == 0);
+ if (base_ && base_ != this && base_->Release()) delete base_;
+}
+
+void Label::Binding::Set(Binding* binding, uint64_t addend) {
+ if (!base_ && !binding) {
+ // We're equating two constants. This could be okay.
+ assert(addend_ == addend);
+ } else if (!base_) {
+ // We are a known constant, but BINDING may not be, so turn the
+ // tables and try to set BINDING's value instead.
+ binding->Set(NULL, addend_ - addend);
+ } else {
+ if (binding) {
+ // Find binding's final value. Since the final value is always either
+ // completely unconstrained or a constant, never a reference to
+ // another variable (otherwise, it wouldn't be final), this
+ // guarantees we won't create cycles here, even for code like this:
+ // l = m, m = n, n = l;
+ uint64_t binding_addend;
+ binding->Get(&binding, &binding_addend);
+ addend += binding_addend;
+ }
+
+ // It seems likely that setting a binding to itself is a bug
+ // (although I can imagine this might turn out to be helpful to
+ // permit).
+ assert(binding != this);
+
+ if (base_ != this) {
+ // Set the other bindings on our chain as well. Note that this
+ // is sufficient even though binding relationships form trees:
+ // All binding operations traverse their chains to the end, and
+ // all bindings related to us share some tail of our chain, so
+ // they will see the changes we make here.
+ base_->Set(binding, addend - addend_);
+ // We're not going to use base_ any more.
+ if (base_->Release()) delete base_;
+ }
+
+ // Adopt BINDING as our base. Note that it should be correct to
+ // acquire here, after the release above, even though the usual
+ // reference-counting rules call for acquiring first, and then
+ // releasing: the self-reference assertion above should have
+ // complained if BINDING were 'this' or anywhere along our chain,
+ // so we didn't release BINDING.
+ if (binding) binding->Acquire();
+ base_ = binding;
+ addend_ = addend;
+ }
+}
+
+void Label::Binding::Get(Binding** base, uint64_t* addend) {
+ if (base_ && base_ != this) {
+ // Recurse to find the end of our reference chain (the root of our
+ // tree), and then rewrite every binding along the chain to refer
+ // to it directly, adjusting addends appropriately. (This is why
+ // this member function isn't this-const.)
+ Binding* final_base;
+ uint64_t final_addend;
+ base_->Get(&final_base, &final_addend);
+ if (final_base) final_base->Acquire();
+ if (base_->Release()) delete base_;
+ base_ = final_base;
+ addend_ += final_addend;
+ }
+ *base = base_;
+ *addend = addend_;
+}
+
+template <typename Inserter>
+static inline void InsertEndian(test_assembler::Endianness endianness,
+ size_t size, uint64_t number, Inserter dest) {
+ assert(size > 0);
+ if (endianness == kLittleEndian) {
+ for (size_t i = 0; i < size; i++) {
+ *dest++ = (char)(number & 0xff);
+ number >>= 8;
+ }
+ } else {
+ assert(endianness == kBigEndian);
+ // The loop condition is odd, but it's correct for size_t.
+ for (size_t i = size - 1; i < size; i--)
+ *dest++ = (char)((number >> (i * 8)) & 0xff);
+ }
+}
+
+Section& Section::Append(Endianness endianness, size_t size, uint64_t number) {
+ InsertEndian(endianness, size, number,
+ back_insert_iterator<string>(contents_));
+ return *this;
+}
+
+Section& Section::Append(Endianness endianness, size_t size,
+ const Label& label) {
+ // If this label's value is known, there's no reason to waste an
+ // entry in references_ on it.
+ uint64_t value;
+ if (label.IsKnownConstant(&value)) return Append(endianness, size, value);
+
+ // This will get caught when the references are resolved, but it's
+ // nicer to find out earlier.
+ assert(endianness != kUnsetEndian);
+
+ references_.push_back(Reference(contents_.size(), endianness, size, label));
+ contents_.append(size, 0);
+ return *this;
+}
+
+#define ENDIANNESS_L kLittleEndian
+#define ENDIANNESS_B kBigEndian
+#define ENDIANNESS(e) ENDIANNESS_##e
+
+#define DEFINE_SHORT_APPEND_NUMBER_ENDIAN(e, bits) \
+ Section& Section::e##bits(uint##bits##_t v) { \
+ InsertEndian(ENDIANNESS(e), bits / 8, v, \
+ back_insert_iterator<string>(contents_)); \
+ return *this; \
+ }
+
+#define DEFINE_SHORT_APPEND_LABEL_ENDIAN(e, bits) \
+ Section& Section::e##bits(const Label& v) { \
+ return Append(ENDIANNESS(e), bits / 8, v); \
+ }
+
+// Define L16, B32, and friends.
+#define DEFINE_SHORT_APPEND_ENDIAN(e, bits) \
+ DEFINE_SHORT_APPEND_NUMBER_ENDIAN(e, bits) \
+ DEFINE_SHORT_APPEND_LABEL_ENDIAN(e, bits)
+
+DEFINE_SHORT_APPEND_LABEL_ENDIAN(L, 8);
+DEFINE_SHORT_APPEND_LABEL_ENDIAN(B, 8);
+DEFINE_SHORT_APPEND_ENDIAN(L, 16);
+DEFINE_SHORT_APPEND_ENDIAN(L, 32);
+DEFINE_SHORT_APPEND_ENDIAN(L, 64);
+DEFINE_SHORT_APPEND_ENDIAN(B, 16);
+DEFINE_SHORT_APPEND_ENDIAN(B, 32);
+DEFINE_SHORT_APPEND_ENDIAN(B, 64);
+
+#define DEFINE_SHORT_APPEND_NUMBER_DEFAULT(bits) \
+ Section& Section::D##bits(uint##bits##_t v) { \
+ InsertEndian(endianness_, bits / 8, v, \
+ back_insert_iterator<string>(contents_)); \
+ return *this; \
+ }
+#define DEFINE_SHORT_APPEND_LABEL_DEFAULT(bits) \
+ Section& Section::D##bits(const Label& v) { \
+ return Append(endianness_, bits / 8, v); \
+ }
+#define DEFINE_SHORT_APPEND_DEFAULT(bits) \
+ DEFINE_SHORT_APPEND_NUMBER_DEFAULT(bits) \
+ DEFINE_SHORT_APPEND_LABEL_DEFAULT(bits)
+
+DEFINE_SHORT_APPEND_LABEL_DEFAULT(8)
+DEFINE_SHORT_APPEND_DEFAULT(16);
+DEFINE_SHORT_APPEND_DEFAULT(32);
+DEFINE_SHORT_APPEND_DEFAULT(64);
+
+Section& Section::LEB128(long long value) {
+ while (value < -0x40 || 0x3f < value) {
+ contents_ += (value & 0x7f) | 0x80;
+ if (value < 0)
+ value = (value >> 7) | ~(((unsigned long long)-1) >> 7);
+ else
+ value = (value >> 7);
+ }
+ contents_ += value & 0x7f;
+ return *this;
+}
+
+Section& Section::ULEB128(uint64_t value) {
+ while (value > 0x7f) {
+ contents_ += (value & 0x7f) | 0x80;
+ value = (value >> 7);
+ }
+ contents_ += value;
+ return *this;
+}
+
+Section& Section::Align(size_t alignment, uint8_t pad_byte) {
+ // ALIGNMENT must be a power of two.
+ assert(((alignment - 1) & alignment) == 0);
+ size_t new_size = (contents_.size() + alignment - 1) & ~(alignment - 1);
+ contents_.append(new_size - contents_.size(), pad_byte);
+ assert((contents_.size() & (alignment - 1)) == 0);
+ return *this;
+}
+
+bool Section::GetContents(string* contents) {
+ // For each label reference, find the label's value, and patch it into
+ // the section's contents.
+ for (size_t i = 0; i < references_.size(); i++) {
+ Reference& r = references_[i];
+ uint64_t value;
+ if (!r.label.IsKnownConstant(&value)) {
+ fprintf(stderr, "Undefined label #%zu at offset 0x%zx\n", i, r.offset);
+ return false;
+ }
+ assert(r.offset < contents_.size());
+ assert(contents_.size() - r.offset >= r.size);
+ InsertEndian(r.endianness, r.size, value, contents_.begin() + r.offset);
+ }
+ contents->clear();
+ std::swap(contents_, *contents);
+ references_.clear();
+ return true;
+}
+
+} // namespace test_assembler
+} // namespace lul_test
+
+namespace lul_test {
+
+CFISection& CFISection::CIEHeader(uint64_t code_alignment_factor,
+ int data_alignment_factor,
+ unsigned return_address_register,
+ uint8_t version, const string& augmentation,
+ bool dwarf64) {
+ assert(!entry_length_);
+ entry_length_ = new PendingLength();
+ in_fde_ = false;
+
+ if (dwarf64) {
+ D32(kDwarf64InitialLengthMarker);
+ D64(entry_length_->length);
+ entry_length_->start = Here();
+ D64(eh_frame_ ? kEHFrame64CIEIdentifier : kDwarf64CIEIdentifier);
+ } else {
+ D32(entry_length_->length);
+ entry_length_->start = Here();
+ D32(eh_frame_ ? kEHFrame32CIEIdentifier : kDwarf32CIEIdentifier);
+ }
+ D8(version);
+ AppendCString(augmentation);
+ ULEB128(code_alignment_factor);
+ LEB128(data_alignment_factor);
+ if (version == 1)
+ D8(return_address_register);
+ else
+ ULEB128(return_address_register);
+ return *this;
+}
+
+CFISection& CFISection::FDEHeader(Label cie_pointer, uint64_t initial_location,
+ uint64_t address_range, bool dwarf64) {
+ assert(!entry_length_);
+ entry_length_ = new PendingLength();
+ in_fde_ = true;
+ fde_start_address_ = initial_location;
+
+ if (dwarf64) {
+ D32(0xffffffff);
+ D64(entry_length_->length);
+ entry_length_->start = Here();
+ if (eh_frame_)
+ D64(Here() - cie_pointer);
+ else
+ D64(cie_pointer);
+ } else {
+ D32(entry_length_->length);
+ entry_length_->start = Here();
+ if (eh_frame_)
+ D32(Here() - cie_pointer);
+ else
+ D32(cie_pointer);
+ }
+ EncodedPointer(initial_location);
+ // The FDE length in an .eh_frame section uses the same encoding as the
+ // initial location, but ignores the base address (selected by the upper
+ // nybble of the encoding), as it's a length, not an address that can be
+ // made relative.
+ EncodedPointer(address_range, DwarfPointerEncoding(pointer_encoding_ & 0x0f));
+ return *this;
+}
+
+CFISection& CFISection::FinishEntry() {
+ assert(entry_length_);
+ Align(address_size_, lul::DW_CFA_nop);
+ entry_length_->length = Here() - entry_length_->start;
+ delete entry_length_;
+ entry_length_ = NULL;
+ in_fde_ = false;
+ return *this;
+}
+
+CFISection& CFISection::EncodedPointer(uint64_t address,
+ DwarfPointerEncoding encoding,
+ const EncodedPointerBases& bases) {
+ // Omitted data is extremely easy to emit.
+ if (encoding == lul::DW_EH_PE_omit) return *this;
+
+ // If (encoding & lul::DW_EH_PE_indirect) != 0, then we assume
+ // that ADDRESS is the address at which the pointer is stored --- in
+ // other words, that bit has no effect on how we write the pointer.
+ encoding = DwarfPointerEncoding(encoding & ~lul::DW_EH_PE_indirect);
+
+ // Find the base address to which this pointer is relative. The upper
+ // nybble of the encoding specifies this.
+ uint64_t base;
+ switch (encoding & 0xf0) {
+ case lul::DW_EH_PE_absptr:
+ base = 0;
+ break;
+ case lul::DW_EH_PE_pcrel:
+ base = bases.cfi + Size();
+ break;
+ case lul::DW_EH_PE_textrel:
+ base = bases.text;
+ break;
+ case lul::DW_EH_PE_datarel:
+ base = bases.data;
+ break;
+ case lul::DW_EH_PE_funcrel:
+ base = fde_start_address_;
+ break;
+ case lul::DW_EH_PE_aligned:
+ base = 0;
+ break;
+ default:
+ abort();
+ };
+
+ // Make ADDRESS relative. Yes, this is appropriate even for "absptr"
+ // values; see gcc/unwind-pe.h.
+ address -= base;
+
+ // Align the pointer, if required.
+ if ((encoding & 0xf0) == lul::DW_EH_PE_aligned) Align(AddressSize());
+
+ // Append ADDRESS to this section in the appropriate form. For the
+ // fixed-width forms, we don't need to differentiate between signed and
+ // unsigned encodings, because ADDRESS has already been extended to 64
+ // bits before it was passed to us.
+ switch (encoding & 0x0f) {
+ case lul::DW_EH_PE_absptr:
+ Address(address);
+ break;
+
+ case lul::DW_EH_PE_uleb128:
+ ULEB128(address);
+ break;
+
+ case lul::DW_EH_PE_sleb128:
+ LEB128(address);
+ break;
+
+ case lul::DW_EH_PE_udata2:
+ case lul::DW_EH_PE_sdata2:
+ D16(address);
+ break;
+
+ case lul::DW_EH_PE_udata4:
+ case lul::DW_EH_PE_sdata4:
+ D32(address);
+ break;
+
+ case lul::DW_EH_PE_udata8:
+ case lul::DW_EH_PE_sdata8:
+ D64(address);
+ break;
+
+ default:
+ abort();
+ }
+
+ return *this;
+};
+
+} // namespace lul_test
diff --git a/tools/profiler/tests/gtest/LulTestInfrastructure.h b/tools/profiler/tests/gtest/LulTestInfrastructure.h
new file mode 100644
index 0000000000..a48353071f
--- /dev/null
+++ b/tools/profiler/tests/gtest/LulTestInfrastructure.h
@@ -0,0 +1,735 @@
+// -*- mode: C++ -*-
+
+// Copyright (c) 2010, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Original author: Jim Blandy <jimb@mozilla.com> <jimb@red-bean.com>
+
+// Derived from:
+// cfi_assembler.h: Define CFISection, a class for creating properly
+// (and improperly) formatted DWARF CFI data for unit tests.
+
+// Derived from:
+// test-assembler.h: interface to class for building complex binary streams.
+
+// To test the Breakpad symbol dumper and processor thoroughly, for
+// all combinations of host system and minidump processor
+// architecture, we need to be able to easily generate complex test
+// data like debugging information and minidump files.
+//
+// For example, if we want our unit tests to provide full code
+// coverage for stack walking, it may be difficult to persuade the
+// compiler to generate every possible sort of stack walking
+// information that we want to support; there are probably DWARF CFI
+// opcodes that GCC never emits. Similarly, if we want to test our
+// error handling, we will need to generate damaged minidumps or
+// debugging information that (we hope) the client or compiler will
+// never produce on its own.
+//
+// google_breakpad::TestAssembler provides a predictable and
+// (relatively) simple way to generate complex formatted data streams
+// like minidumps and CFI. Furthermore, because TestAssembler is
+// portable, developers without access to (say) Visual Studio or a
+// SPARC assembler can still work on test data for those targets.
+
+#ifndef LUL_TEST_INFRASTRUCTURE_H
+#define LUL_TEST_INFRASTRUCTURE_H
+
+#include "LulDwarfExt.h"
+
+#include <string>
+#include <vector>
+
+using std::string;
+using std::vector;
+
+namespace lul_test {
+namespace test_assembler {
+
+// A Label represents a value not yet known that we need to store in a
+// section. As long as all the labels a section refers to are defined
+// by the time we retrieve its contents as bytes, we can use undefined
+// labels freely in that section's construction.
+//
+// A label can be in one of three states:
+// - undefined,
+// - defined as the sum of some other label and a constant, or
+// - a constant.
+//
+// A label's value never changes, but it can accumulate constraints.
+// Adding labels and integers is permitted, and yields a label.
+// Subtracting a constant from a label is permitted, and also yields a
+// label. Subtracting two labels that have some relationship to each
+// other is permitted, and yields a constant.
+//
+// For example:
+//
+// Label a; // a's value is undefined
+// Label b; // b's value is undefined
+// {
+// Label c = a + 4; // okay, even though a's value is unknown
+// b = c + 4; // also okay; b is now a+8
+// }
+// Label d = b - 2; // okay; d == a+6, even though c is gone
+// d.Value(); // error: d's value is not yet known
+// d - a; // is 6, even though their values are not known
+// a = 12; // now b == 20, and d == 18
+// d.Value(); // 18: no longer an error
+// b.Value(); // 20
+// d = 10; // error: d is already defined.
+//
+// Label objects' lifetimes are unconstrained: notice that, in the
+// above example, even though a and b are only related through c, and
+// c goes out of scope, the assignment to a sets b's value as well. In
+// particular, it's not necessary to ensure that a Label lives beyond
+// Sections that refer to it.
+class Label {
+ public:
+ Label(); // An undefined label.
+ explicit Label(uint64_t value); // A label with a fixed value
+ Label(const Label& value); // A label equal to another.
+ ~Label();
+
+ Label& operator=(uint64_t value);
+ Label& operator=(const Label& value);
+ Label operator+(uint64_t addend) const;
+ Label operator-(uint64_t subtrahend) const;
+ uint64_t operator-(const Label& subtrahend) const;
+
+ // We could also provide == and != that work on undefined, but
+ // related, labels.
+
+ // Return true if this label's value is known. If VALUE_P is given,
+ // set *VALUE_P to the known value if returning true.
+ bool IsKnownConstant(uint64_t* value_p = NULL) const;
+
+ // Return true if the offset from LABEL to this label is known. If
+ // OFFSET_P is given, set *OFFSET_P to the offset when returning true.
+ //
+ // You can think of l.KnownOffsetFrom(m, &d) as being like 'd = l-m',
+ // except that it also returns a value indicating whether the
+ // subtraction is possible given what we currently know of l and m.
+ // It can be possible even if we don't know l and m's values. For
+ // example:
+ //
+ // Label l, m;
+ // m = l + 10;
+ // l.IsKnownConstant(); // false
+ // m.IsKnownConstant(); // false
+ // uint64_t d;
+ // l.IsKnownOffsetFrom(m, &d); // true, and sets d to -10.
+ // l-m // -10
+ // m-l // 10
+ // m.Value() // error: m's value is not known
+ bool IsKnownOffsetFrom(const Label& label, uint64_t* offset_p = NULL) const;
+
+ private:
+ // A label's value, or if that is not yet known, how the value is
+ // related to other labels' values. A binding may be:
+ // - a known constant,
+ // - constrained to be equal to some other binding plus a constant, or
+ // - unconstrained, and free to take on any value.
+ //
+ // Many labels may point to a single binding, and each binding may
+ // refer to another, so bindings and labels form trees whose leaves
+ // are labels, whose interior nodes (and roots) are bindings, and
+ // where links point from children to parents. Bindings are
+ // reference counted, allowing labels to be lightweight, copyable,
+ // assignable, placed in containers, and so on.
+ class Binding {
+ public:
+ Binding();
+ explicit Binding(uint64_t addend);
+ ~Binding();
+
+ // Increment our reference count.
+ void Acquire() { reference_count_++; };
+ // Decrement our reference count, and return true if it is zero.
+ bool Release() { return --reference_count_ == 0; }
+
+ // Set this binding to be equal to BINDING + ADDEND. If BINDING is
+ // NULL, then set this binding to the known constant ADDEND.
+ // Update every binding on this binding's chain to point directly
+ // to BINDING, or to be a constant, with addends adjusted
+ // appropriately.
+ void Set(Binding* binding, uint64_t value);
+
+ // Return what we know about the value of this binding.
+ // - If this binding's value is a known constant, set BASE to
+ // NULL, and set ADDEND to its value.
+ // - If this binding is not a known constant but related to other
+ // bindings, set BASE to the binding at the end of the relation
+ // chain (which will always be unconstrained), and set ADDEND to the
+ // value to add to that binding's value to get this binding's
+ // value.
+ // - If this binding is unconstrained, set BASE to this, and leave
+ // ADDEND unchanged.
+ void Get(Binding** base, uint64_t* addend);
+
+ private:
+ // There are three cases:
+ //
+ // - A binding representing a known constant value has base_ NULL,
+ // and addend_ equal to the value.
+ //
+ // - A binding representing a completely unconstrained value has
+ // base_ pointing to this; addend_ is unused.
+ //
+ // - A binding whose value is related to some other binding's
+ // value has base_ pointing to that other binding, and addend_
+ // set to the amount to add to that binding's value to get this
+ // binding's value. We only represent relationships of the form
+ // x = y+c.
+ //
+ // Thus, the bind_ links form a chain terminating in either a
+ // known constant value or a completely unconstrained value. Most
+ // operations on bindings do path compression: they change every
+ // binding on the chain to point directly to the final value,
+ // adjusting addends as appropriate.
+ Binding* base_;
+ uint64_t addend_;
+
+ // The number of Labels and Bindings pointing to this binding.
+ // (When a binding points to itself, indicating a completely
+ // unconstrained binding, that doesn't count as a reference.)
+ int reference_count_;
+ };
+
+ // This label's value.
+ Binding* value_;
+};
+
+// Conventions for representing larger numbers as sequences of bytes.
+enum Endianness {
+ kBigEndian, // Big-endian: the most significant byte comes first.
+ kLittleEndian, // Little-endian: the least significant byte comes first.
+ kUnsetEndian, // used internally
+};
+
+// A section is a sequence of bytes, constructed by appending bytes
+// to the end. Sections have a convenient and flexible set of member
+// functions for appending data in various formats: big-endian and
+// little-endian signed and unsigned values of different sizes;
+// LEB128 and ULEB128 values (see below), and raw blocks of bytes.
+//
+// If you need to append a value to a section that is not convenient
+// to compute immediately, you can create a label, append the
+// label's value to the section, and then set the label's value
+// later, when it's convenient to do so. Once a label's value is
+// known, the section class takes care of updating all previously
+// appended references to it.
+//
+// Once all the labels to which a section refers have had their
+// values determined, you can get a copy of the section's contents
+// as a string.
+//
+// Note that there is no specified "start of section" label. This is
+// because there are typically several different meanings for "the
+// start of a section": the offset of the section within an object
+// file, the address in memory at which the section's content appear,
+// and so on. It's up to the code that uses the Section class to
+// keep track of these explicitly, as they depend on the application.
+class Section {
+ public:
+ explicit Section(Endianness endianness = kUnsetEndian)
+ : endianness_(endianness){};
+
+ // A base class destructor should be either public and virtual,
+ // or protected and nonvirtual.
+ virtual ~Section(){};
+
+ // Return the default endianness of this section.
+ Endianness endianness() const { return endianness_; }
+
+ // Append the SIZE bytes at DATA to the end of this section. Return
+ // a reference to this section.
+ Section& Append(const string& data) {
+ contents_.append(data);
+ return *this;
+ };
+
+ // Append data from SLICE to the end of this section. Return
+ // a reference to this section.
+ Section& Append(const lul::ImageSlice& slice) {
+ for (size_t i = 0; i < slice.length_; i++) {
+ contents_.append(1, slice.start_[i]);
+ }
+ return *this;
+ }
+
+ // Append data from CSTRING to the end of this section. The terminating
+ // zero is not included. Return a reference to this section.
+ Section& Append(const char* cstring) {
+ for (size_t i = 0; cstring[i] != '\0'; i++) {
+ contents_.append(1, cstring[i]);
+ }
+ return *this;
+ }
+
+ // Append SIZE copies of BYTE to the end of this section. Return a
+ // reference to this section.
+ Section& Append(size_t size, uint8_t byte) {
+ contents_.append(size, (char)byte);
+ return *this;
+ }
+
+ // Append NUMBER to this section. ENDIANNESS is the endianness to
+ // use to write the number. SIZE is the length of the number in
+ // bytes. Return a reference to this section.
+ Section& Append(Endianness endianness, size_t size, uint64_t number);
+ Section& Append(Endianness endianness, size_t size, const Label& label);
+
+ // Append SECTION to the end of this section. The labels SECTION
+ // refers to need not be defined yet.
+ //
+ // Note that this has no effect on any Labels' values, or on
+ // SECTION. If placing SECTION within 'this' provides new
+ // constraints on existing labels' values, then it's up to the
+ // caller to fiddle with those labels as needed.
+ Section& Append(const Section& section);
+
+ // Append the contents of DATA as a series of bytes terminated by
+ // a NULL character.
+ Section& AppendCString(const string& data) {
+ Append(data);
+ contents_ += '\0';
+ return *this;
+ }
+
+ // Append VALUE or LABEL to this section, with the given bit width and
+ // endianness. Return a reference to this section.
+ //
+ // The names of these functions have the form <ENDIANNESS><BITWIDTH>:
+ // <ENDIANNESS> is either 'L' (little-endian, least significant byte first),
+ // 'B' (big-endian, most significant byte first), or
+ // 'D' (default, the section's default endianness)
+ // <BITWIDTH> is 8, 16, 32, or 64.
+ //
+ // Since endianness doesn't matter for a single byte, all the
+ // <BITWIDTH>=8 functions are equivalent.
+ //
+ // These can be used to write both signed and unsigned values, as
+ // the compiler will properly sign-extend a signed value before
+ // passing it to the function, at which point the function's
+ // behavior is the same either way.
+ Section& L8(uint8_t value) {
+ contents_ += value;
+ return *this;
+ }
+ Section& B8(uint8_t value) {
+ contents_ += value;
+ return *this;
+ }
+ Section& D8(uint8_t value) {
+ contents_ += value;
+ return *this;
+ }
+ Section &L16(uint16_t), &L32(uint32_t), &L64(uint64_t), &B16(uint16_t),
+ &B32(uint32_t), &B64(uint64_t), &D16(uint16_t), &D32(uint32_t),
+ &D64(uint64_t);
+ Section &L8(const Label&label), &L16(const Label&label),
+ &L32(const Label&label), &L64(const Label&label), &B8(const Label&label),
+ &B16(const Label&label), &B32(const Label&label), &B64(const Label&label),
+ &D8(const Label&label), &D16(const Label&label), &D32(const Label&label),
+ &D64(const Label&label);
+
+ // Append VALUE in a signed LEB128 (Little-Endian Base 128) form.
+ //
+ // The signed LEB128 representation of an integer N is a variable
+ // number of bytes:
+ //
+ // - If N is between -0x40 and 0x3f, then its signed LEB128
+ // representation is a single byte whose value is N.
+ //
+ // - Otherwise, its signed LEB128 representation is (N & 0x7f) |
+ // 0x80, followed by the signed LEB128 representation of N / 128,
+ // rounded towards negative infinity.
+ //
+ // In other words, we break VALUE into groups of seven bits, put
+ // them in little-endian order, and then write them as eight-bit
+ // bytes with the high bit on all but the last.
+ //
+ // Note that VALUE cannot be a Label (we would have to implement
+ // relaxation).
+ Section& LEB128(long long value);
+
+ // Append VALUE in unsigned LEB128 (Little-Endian Base 128) form.
+ //
+ // The unsigned LEB128 representation of an integer N is a variable
+ // number of bytes:
+ //
+ // - If N is between 0 and 0x7f, then its unsigned LEB128
+ // representation is a single byte whose value is N.
+ //
+ // - Otherwise, its unsigned LEB128 representation is (N & 0x7f) |
+ // 0x80, followed by the unsigned LEB128 representation of N /
+ // 128, rounded towards negative infinity.
+ //
+ // Note that VALUE cannot be a Label (we would have to implement
+ // relaxation).
+ Section& ULEB128(uint64_t value);
+
+ // Jump to the next location aligned on an ALIGNMENT-byte boundary,
+ // relative to the start of the section. Fill the gap with PAD_BYTE.
+ // ALIGNMENT must be a power of two. Return a reference to this
+ // section.
+ Section& Align(size_t alignment, uint8_t pad_byte = 0);
+
+ // Return the current size of the section.
+ size_t Size() const { return contents_.size(); }
+
+ // Return a label representing the start of the section.
+ //
+ // It is up to the user whether this label represents the section's
+ // position in an object file, the section's address in memory, or
+ // what have you; some applications may need both, in which case
+ // this simple-minded interface won't be enough. This class only
+ // provides a single start label, for use with the Here and Mark
+ // member functions.
+ //
+ // Ideally, we'd provide this in a subclass that actually knows more
+ // about the application at hand and can provide an appropriate
+ // collection of start labels. But then the appending member
+ // functions like Append and D32 would return a reference to the
+ // base class, not the derived class, and the chaining won't work.
+ // Since the only value here is in pretty notation, that's a fatal
+ // flaw.
+ Label start() const { return start_; }
+
+ // Return a label representing the point at which the next Appended
+ // item will appear in the section, relative to start().
+ Label Here() const { return start_ + Size(); }
+
+ // Set *LABEL to Here, and return a reference to this section.
+ Section& Mark(Label* label) {
+ *label = Here();
+ return *this;
+ }
+
+ // If there are no undefined label references left in this
+ // section, set CONTENTS to the contents of this section, as a
+ // string, and clear this section. Return true on success, or false
+ // if there were still undefined labels.
+ bool GetContents(string* contents);
+
+ private:
+ // Used internally. A reference to a label's value.
+ struct Reference {
+ Reference(size_t set_offset, Endianness set_endianness, size_t set_size,
+ const Label& set_label)
+ : offset(set_offset),
+ endianness(set_endianness),
+ size(set_size),
+ label(set_label) {}
+
+ // The offset of the reference within the section.
+ size_t offset;
+
+ // The endianness of the reference.
+ Endianness endianness;
+
+ // The size of the reference.
+ size_t size;
+
+ // The label to which this is a reference.
+ Label label;
+ };
+
+ // The default endianness of this section.
+ Endianness endianness_;
+
+ // The contents of the section.
+ string contents_;
+
+ // References to labels within those contents.
+ vector<Reference> references_;
+
+ // A label referring to the beginning of the section.
+ Label start_;
+};
+
+} // namespace test_assembler
+} // namespace lul_test
+
+namespace lul_test {
+
+using lul::DwarfPointerEncoding;
+using lul_test::test_assembler::Endianness;
+using lul_test::test_assembler::Label;
+using lul_test::test_assembler::Section;
+
+class CFISection : public Section {
+ public:
+ // CFI augmentation strings beginning with 'z', defined by the
+ // Linux/IA-64 C++ ABI, can specify interesting encodings for
+ // addresses appearing in FDE headers and call frame instructions (and
+ // for additional fields whose presence the augmentation string
+ // specifies). In particular, pointers can be specified to be relative
+ // to various base address: the start of the .text section, the
+ // location holding the address itself, and so on. These allow the
+ // frame data to be position-independent even when they live in
+ // write-protected pages. These variants are specified at the
+ // following two URLs:
+ //
+ // http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/dwarfext.html
+ // http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html
+ //
+ // CFISection leaves the production of well-formed 'z'-augmented CIEs and
+ // FDEs to the user, but does provide EncodedPointer, to emit
+ // properly-encoded addresses for a given pointer encoding.
+ // EncodedPointer uses an instance of this structure to find the base
+ // addresses it should use; you can establish a default for all encoded
+ // pointers appended to this section with SetEncodedPointerBases.
+ struct EncodedPointerBases {
+ EncodedPointerBases() : cfi(), text(), data() {}
+
+ // The starting address of this CFI section in memory, for
+ // DW_EH_PE_pcrel. DW_EH_PE_pcrel pointers may only be used in data
+ // that has is loaded into the program's address space.
+ uint64_t cfi;
+
+ // The starting address of this file's .text section, for DW_EH_PE_textrel.
+ uint64_t text;
+
+ // The starting address of this file's .got or .eh_frame_hdr section,
+ // for DW_EH_PE_datarel.
+ uint64_t data;
+ };
+
+ // Create a CFISection whose endianness is ENDIANNESS, and where
+ // machine addresses are ADDRESS_SIZE bytes long. If EH_FRAME is
+ // true, use the .eh_frame format, as described by the Linux
+ // Standards Base Core Specification, instead of the DWARF CFI
+ // format.
+ CFISection(Endianness endianness, size_t address_size, bool eh_frame = false)
+ : Section(endianness),
+ address_size_(address_size),
+ eh_frame_(eh_frame),
+ pointer_encoding_(lul::DW_EH_PE_absptr),
+ encoded_pointer_bases_(),
+ entry_length_(NULL),
+ in_fde_(false) {
+ // The 'start', 'Here', and 'Mark' members of a CFISection all refer
+ // to section offsets.
+ start() = 0;
+ }
+
+ // Return this CFISection's address size.
+ size_t AddressSize() const { return address_size_; }
+
+ // Return true if this CFISection uses the .eh_frame format, or
+ // false if it contains ordinary DWARF CFI data.
+ bool ContainsEHFrame() const { return eh_frame_; }
+
+ // Use ENCODING for pointers in calls to FDEHeader and EncodedPointer.
+ void SetPointerEncoding(DwarfPointerEncoding encoding) {
+ pointer_encoding_ = encoding;
+ }
+
+ // Use the addresses in BASES as the base addresses for encoded
+ // pointers in subsequent calls to FDEHeader or EncodedPointer.
+ // This function makes a copy of BASES.
+ void SetEncodedPointerBases(const EncodedPointerBases& bases) {
+ encoded_pointer_bases_ = bases;
+ }
+
+ // Append a Common Information Entry header to this section with the
+ // given values. If dwarf64 is true, use the 64-bit DWARF initial
+ // length format for the CIE's initial length. Return a reference to
+ // this section. You should call FinishEntry after writing the last
+ // instruction for the CIE.
+ //
+ // Before calling this function, you will typically want to use Mark
+ // or Here to make a label to pass to FDEHeader that refers to this
+ // CIE's position in the section.
+ CFISection& CIEHeader(uint64_t code_alignment_factor,
+ int data_alignment_factor,
+ unsigned return_address_register, uint8_t version = 3,
+ const string& augmentation = "", bool dwarf64 = false);
+
+ // Append a Frame Description Entry header to this section with the
+ // given values. If dwarf64 is true, use the 64-bit DWARF initial
+ // length format for the CIE's initial length. Return a reference to
+ // this section. You should call FinishEntry after writing the last
+ // instruction for the CIE.
+ //
+ // This function doesn't support entries that are longer than
+ // 0xffffff00 bytes. (The "initial length" is always a 32-bit
+ // value.) Nor does it support .debug_frame sections longer than
+ // 0xffffff00 bytes.
+ CFISection& FDEHeader(Label cie_pointer, uint64_t initial_location,
+ uint64_t address_range, bool dwarf64 = false);
+
+ // Note the current position as the end of the last CIE or FDE we
+ // started, after padding with DW_CFA_nops for alignment. This
+ // defines the label representing the entry's length, cited in the
+ // entry's header. Return a reference to this section.
+ CFISection& FinishEntry();
+
+ // Append the contents of BLOCK as a DW_FORM_block value: an
+ // unsigned LEB128 length, followed by that many bytes of data.
+ CFISection& Block(const lul::ImageSlice& block) {
+ ULEB128(block.length_);
+ Append(block);
+ return *this;
+ }
+
+ // Append data from CSTRING as a DW_FORM_block value: an unsigned LEB128
+ // length, followed by that many bytes of data. The terminating zero is not
+ // included.
+ CFISection& Block(const char* cstring) {
+ ULEB128(strlen(cstring));
+ Append(cstring);
+ return *this;
+ }
+
+ // Append ADDRESS to this section, in the appropriate size and
+ // endianness. Return a reference to this section.
+ CFISection& Address(uint64_t address) {
+ Section::Append(endianness(), address_size_, address);
+ return *this;
+ }
+
+ // Append ADDRESS to this section, using ENCODING and BASES. ENCODING
+ // defaults to this section's default encoding, established by
+ // SetPointerEncoding. BASES defaults to this section's bases, set by
+ // SetEncodedPointerBases. If the DW_EH_PE_indirect bit is set in the
+ // encoding, assume that ADDRESS is where the true address is stored.
+ // Return a reference to this section.
+ //
+ // (C++ doesn't let me use default arguments here, because I want to
+ // refer to members of *this in the default argument expression.)
+ CFISection& EncodedPointer(uint64_t address) {
+ return EncodedPointer(address, pointer_encoding_, encoded_pointer_bases_);
+ }
+ CFISection& EncodedPointer(uint64_t address, DwarfPointerEncoding encoding) {
+ return EncodedPointer(address, encoding, encoded_pointer_bases_);
+ }
+ CFISection& EncodedPointer(uint64_t address, DwarfPointerEncoding encoding,
+ const EncodedPointerBases& bases);
+
+ // Restate some member functions, to keep chaining working nicely.
+ CFISection& Mark(Label* label) {
+ Section::Mark(label);
+ return *this;
+ }
+ CFISection& D8(uint8_t v) {
+ Section::D8(v);
+ return *this;
+ }
+ CFISection& D16(uint16_t v) {
+ Section::D16(v);
+ return *this;
+ }
+ CFISection& D16(Label v) {
+ Section::D16(v);
+ return *this;
+ }
+ CFISection& D32(uint32_t v) {
+ Section::D32(v);
+ return *this;
+ }
+ CFISection& D32(const Label& v) {
+ Section::D32(v);
+ return *this;
+ }
+ CFISection& D64(uint64_t v) {
+ Section::D64(v);
+ return *this;
+ }
+ CFISection& D64(const Label& v) {
+ Section::D64(v);
+ return *this;
+ }
+ CFISection& LEB128(long long v) {
+ Section::LEB128(v);
+ return *this;
+ }
+ CFISection& ULEB128(uint64_t v) {
+ Section::ULEB128(v);
+ return *this;
+ }
+
+ private:
+ // A length value that we've appended to the section, but is not yet
+ // known. LENGTH is the appended value; START is a label referring
+ // to the start of the data whose length was cited.
+ struct PendingLength {
+ Label length;
+ Label start;
+ };
+
+ // Constants used in CFI/.eh_frame data:
+
+ // If the first four bytes of an "initial length" are this constant, then
+ // the data uses the 64-bit DWARF format, and the length itself is the
+ // subsequent eight bytes.
+ static const uint32_t kDwarf64InitialLengthMarker = 0xffffffffU;
+
+ // The CIE identifier for 32- and 64-bit DWARF CFI and .eh_frame data.
+ static const uint32_t kDwarf32CIEIdentifier = ~(uint32_t)0;
+ static const uint64_t kDwarf64CIEIdentifier = ~(uint64_t)0;
+ static const uint32_t kEHFrame32CIEIdentifier = 0;
+ static const uint64_t kEHFrame64CIEIdentifier = 0;
+
+ // The size of a machine address for the data in this section.
+ size_t address_size_;
+
+ // If true, we are generating a Linux .eh_frame section, instead of
+ // a standard DWARF .debug_frame section.
+ bool eh_frame_;
+
+ // The encoding to use for FDE pointers.
+ DwarfPointerEncoding pointer_encoding_;
+
+ // The base addresses to use when emitting encoded pointers.
+ EncodedPointerBases encoded_pointer_bases_;
+
+ // The length value for the current entry.
+ //
+ // Oddly, this must be dynamically allocated. Labels never get new
+ // values; they only acquire constraints on the value they already
+ // have, or assert if you assign them something incompatible. So
+ // each header needs truly fresh Label objects to cite in their
+ // headers and track their positions. The alternative is explicit
+ // destructor invocation and a placement new. Ick.
+ PendingLength* entry_length_;
+
+ // True if we are currently emitting an FDE --- that is, we have
+ // called FDEHeader but have not yet called FinishEntry.
+ bool in_fde_;
+
+ // If in_fde_ is true, this is its starting address. We use this for
+ // emitting DW_EH_PE_funcrel pointers.
+ uint64_t fde_start_address_;
+};
+
+} // namespace lul_test
+
+#endif // LUL_TEST_INFRASTRUCTURE_H
diff --git a/tools/profiler/tests/gtest/ThreadProfileTest.cpp b/tools/profiler/tests/gtest/ThreadProfileTest.cpp
new file mode 100644
index 0000000000..b8a15c39b2
--- /dev/null
+++ b/tools/profiler/tests/gtest/ThreadProfileTest.cpp
@@ -0,0 +1,60 @@
+
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifdef MOZ_GECKO_PROFILER
+
+# include "ProfileBuffer.h"
+
+# include "mozilla/PowerOfTwo.h"
+# include "mozilla/ProfileBufferChunkManagerWithLocalLimit.h"
+# include "mozilla/ProfileChunkedBuffer.h"
+
+# include "gtest/gtest.h"
+
+// Make sure we can record one entry and read it
+TEST(ThreadProfile, InsertOneEntry)
+{
+ mozilla::ProfileBufferChunkManagerWithLocalLimit chunkManager(
+ 2 * (1 + uint32_t(sizeof(ProfileBufferEntry))) * 4,
+ 2 * (1 + uint32_t(sizeof(ProfileBufferEntry))));
+ mozilla::ProfileChunkedBuffer profileChunkedBuffer(
+ mozilla::ProfileChunkedBuffer::ThreadSafety::WithMutex, chunkManager);
+ auto pb = mozilla::MakeUnique<ProfileBuffer>(profileChunkedBuffer);
+ pb->AddEntry(ProfileBufferEntry::Time(123.1));
+ ProfileBufferEntry entry = pb->GetEntry(pb->BufferRangeStart());
+ ASSERT_TRUE(entry.IsTime());
+ ASSERT_EQ(123.1, entry.GetDouble());
+}
+
+// See if we can insert some entries
+TEST(ThreadProfile, InsertEntriesNoWrap)
+{
+ mozilla::ProfileBufferChunkManagerWithLocalLimit chunkManager(
+ 100 * (1 + uint32_t(sizeof(ProfileBufferEntry))),
+ 100 * (1 + uint32_t(sizeof(ProfileBufferEntry))) / 4);
+ mozilla::ProfileChunkedBuffer profileChunkedBuffer(
+ mozilla::ProfileChunkedBuffer::ThreadSafety::WithMutex, chunkManager);
+ auto pb = mozilla::MakeUnique<ProfileBuffer>(profileChunkedBuffer);
+ const int test_size = 50;
+ for (int i = 0; i < test_size; i++) {
+ pb->AddEntry(ProfileBufferEntry::Time(i));
+ }
+ int times = 0;
+ uint64_t readPos = pb->BufferRangeStart();
+ while (readPos != pb->BufferRangeEnd()) {
+ ProfileBufferEntry entry = pb->GetEntry(readPos);
+ readPos++;
+ if (entry.GetKind() == ProfileBufferEntry::Kind::INVALID) {
+ continue;
+ }
+ ASSERT_TRUE(entry.IsTime());
+ ASSERT_EQ(times, entry.GetDouble());
+ times++;
+ }
+ ASSERT_EQ(test_size, times);
+}
+
+#endif // MOZ_GECKO_PROFILER
diff --git a/tools/profiler/tests/gtest/moz.build b/tools/profiler/tests/gtest/moz.build
new file mode 100644
index 0000000000..4eb1fef762
--- /dev/null
+++ b/tools/profiler/tests/gtest/moz.build
@@ -0,0 +1,45 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if (
+ CONFIG["MOZ_GECKO_PROFILER"]
+ and CONFIG["OS_TARGET"] in ("Android", "Linux")
+ and CONFIG["CPU_ARCH"]
+ in (
+ "arm",
+ "aarch64",
+ "x86",
+ "x86_64",
+ )
+):
+ UNIFIED_SOURCES += [
+ "LulTest.cpp",
+ "LulTestDwarf.cpp",
+ "LulTestInfrastructure.cpp",
+ ]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+ "/netwerk/protocol/http",
+ "/toolkit/components/jsoncpp/include",
+ "/tools/profiler/core",
+ "/tools/profiler/gecko",
+ "/tools/profiler/lul",
+]
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "GeckoProfiler.cpp",
+ "ThreadProfileTest.cpp",
+ ]
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/tools/profiler/tests/shared-head.js b/tools/profiler/tests/shared-head.js
new file mode 100644
index 0000000000..3b18934d01
--- /dev/null
+++ b/tools/profiler/tests/shared-head.js
@@ -0,0 +1,568 @@
+/* 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/. */
+
+/* globals Assert */
+
+/**
+ * This file contains utilities that can be shared between xpcshell tests and mochitests.
+ */
+
+// The marker phases.
+const INSTANT = 0;
+const INTERVAL = 1;
+const INTERVAL_START = 2;
+const INTERVAL_END = 3;
+
+// This Services declaration may shadow another from head.js, so define it as
+// a var rather than a const.
+
+const defaultSettings = {
+ entries: 8 * 1024 * 1024, // 8M entries = 64MB
+ interval: 1, // ms
+ features: [],
+ threads: ["GeckoMain"],
+};
+
+// Effectively `async`: Start the profiler and return the `startProfiler`
+// promise that will get resolved when all child process have started their own
+// profiler.
+function startProfiler(callersSettings) {
+ if (Services.profiler.IsActive()) {
+ throw new Error(
+ "The profiler must not be active before starting it in a test."
+ );
+ }
+ const settings = Object.assign({}, defaultSettings, callersSettings);
+ return Services.profiler.StartProfiler(
+ settings.entries,
+ settings.interval,
+ settings.features,
+ settings.threads,
+ 0,
+ settings.duration
+ );
+}
+
+function startProfilerForMarkerTests() {
+ return startProfiler({
+ features: ["nostacksampling", "js"],
+ threads: ["GeckoMain", "DOM Worker"],
+ });
+}
+
+/**
+ * This is a helper function be able to run `await wait(500)`. Unfortunately
+ * this is needed as the act of collecting functions relies on the periodic
+ * sampling of the threads. See:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1529053
+ *
+ * @param {number} time
+ * @returns {Promise}
+ */
+function wait(time) {
+ return new Promise(resolve => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, time);
+ });
+}
+
+/**
+ * Get the payloads of a type recursively, including from all subprocesses.
+ *
+ * @param {Object} profile The gecko profile.
+ * @param {string} type The marker payload type, e.g. "DiskIO".
+ * @param {Array} payloadTarget The recursive list of payloads.
+ * @return {Array} The final payloads.
+ */
+function getPayloadsOfTypeFromAllThreads(profile, type, payloadTarget = []) {
+ for (const { markers } of profile.threads) {
+ for (const markerTuple of markers.data) {
+ const payload = markerTuple[markers.schema.data];
+ if (payload && payload.type === type) {
+ payloadTarget.push(payload);
+ }
+ }
+ }
+
+ for (const subProcess of profile.processes) {
+ getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
+ }
+
+ return payloadTarget;
+}
+
+/**
+ * Get the payloads of a type from a single thread.
+ *
+ * @param {Object} thread The thread from a profile.
+ * @param {string} type The marker payload type, e.g. "DiskIO".
+ * @return {Array} The payloads.
+ */
+function getPayloadsOfType(thread, type) {
+ const { markers } = thread;
+ const results = [];
+ for (const markerTuple of markers.data) {
+ const payload = markerTuple[markers.schema.data];
+ if (payload && payload.type === type) {
+ results.push(payload);
+ }
+ }
+ return results;
+}
+
+/**
+ * Applies the marker schema to create individual objects for each marker
+ *
+ * @param {Object} thread The thread from a profile.
+ * @return {InflatedMarker[]} The markers.
+ */
+function getInflatedMarkerData(thread) {
+ const { markers, stringTable } = thread;
+ return markers.data.map(markerTuple => {
+ const marker = {};
+ for (const [key, tupleIndex] of Object.entries(markers.schema)) {
+ marker[key] = markerTuple[tupleIndex];
+ if (key === "name") {
+ // Use the string from the string table.
+ marker[key] = stringTable[marker[key]];
+ }
+ }
+ return marker;
+ });
+}
+
+/**
+ * Applies the marker schema to create individual objects for each marker, then
+ * keeps only the network markers that match the profiler tests.
+ *
+ * @param {Object} thread The thread from a profile.
+ * @return {InflatedMarker[]} The filtered network markers.
+ */
+function getInflatedNetworkMarkers(thread) {
+ const markers = getInflatedMarkerData(thread);
+ return markers.filter(
+ m =>
+ m.data &&
+ m.data.type === "Network" &&
+ // We filter out network markers that aren't related to the test, to
+ // avoid intermittents.
+ m.data.URI.includes("/tools/profiler/")
+ );
+}
+
+/**
+ * From a list of network markers, this returns pairs of start/stop markers.
+ * If a stop marker can't be found for a start marker, this will return an array
+ * of only 1 element.
+ *
+ * @param {InflatedMarker[]} networkMarkers Network markers
+ * @return {InflatedMarker[][]} Pairs of network markers
+ */
+function getPairsOfNetworkMarkers(allNetworkMarkers) {
+ // For each 'start' marker we want to find the next 'stop' or 'redirect'
+ // marker with the same id.
+ const result = [];
+ const mapOfStartMarkers = new Map(); // marker id -> id in result array
+ for (const marker of allNetworkMarkers) {
+ const { data } = marker;
+ if (data.status === "STATUS_START") {
+ if (mapOfStartMarkers.has(data.id)) {
+ const previousMarker = result[mapOfStartMarkers.get(data.id)][0];
+ Assert.ok(
+ false,
+ `We found 2 start markers with the same id ${data.id}, without end marker in-between.` +
+ `The first marker has URI ${previousMarker.data.URI}, the second marker has URI ${data.URI}.` +
+ ` This should not happen.`
+ );
+ continue;
+ }
+
+ mapOfStartMarkers.set(data.id, result.length);
+ result.push([marker]);
+ } else {
+ // STOP or REDIRECT
+ if (!mapOfStartMarkers.has(data.id)) {
+ Assert.ok(
+ false,
+ `We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
+ );
+ continue;
+ }
+ result[mapOfStartMarkers.get(data.id)].push(marker);
+ mapOfStartMarkers.delete(data.id);
+ }
+ }
+
+ return result;
+}
+
+/**
+ * It can be helpful to force the profiler to collect a JavaScript sample. This
+ * function spins on a while loop until at least one more sample is collected.
+ *
+ * @return {number} The index of the collected sample.
+ */
+function captureAtLeastOneJsSample() {
+ function getProfileSampleCount() {
+ const profile = Services.profiler.getProfileData();
+ return profile.threads[0].samples.data.length;
+ }
+
+ const sampleCount = getProfileSampleCount();
+ // Create an infinite loop until a sample has been collected.
+ while (true) {
+ if (sampleCount < getProfileSampleCount()) {
+ return sampleCount;
+ }
+ }
+}
+
+function isJSONWhitespace(c) {
+ return ["\n", "\r", " ", "\t"].includes(c);
+}
+
+function verifyJSONStringIsCompact(s) {
+ const stateData = 0;
+ const stateString = 1;
+ const stateEscapedChar = 2;
+ let state = stateData;
+ for (let i = 0; i < s.length; ++i) {
+ let c = s[i];
+ switch (state) {
+ case stateData:
+ if (isJSONWhitespace(c)) {
+ Assert.ok(
+ false,
+ `"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
+ );
+ return;
+ }
+ if (c == '"') {
+ state = stateString;
+ }
+ break;
+ case stateString:
+ if (c == '"') {
+ state = stateData;
+ } else if (c == "\\") {
+ state = stateEscapedChar;
+ }
+ break;
+ case stateEscapedChar:
+ state = stateString;
+ break;
+ }
+ }
+}
+
+/**
+ * This function pauses the profiler before getting the profile. Then after
+ * getting the data, the profiler is stopped, and all profiler data is removed.
+ * @returns {Promise<Profile>}
+ */
+async function stopNowAndGetProfile() {
+ // Don't await the pause, because each process will handle it before it
+ // receives the following `getProfileDataAsArrayBuffer()`.
+ Services.profiler.Pause();
+
+ const profileArrayBuffer = await Services.profiler.getProfileDataAsArrayBuffer();
+ await Services.profiler.StopProfiler();
+
+ const profileUint8Array = new Uint8Array(profileArrayBuffer);
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
+ const profileString = textDecoder.decode(profileUint8Array);
+ verifyJSONStringIsCompact(profileString);
+
+ return JSON.parse(profileString);
+}
+
+/**
+ * This function ensures there's at least one sample, then pauses the profiler
+ * before getting the profile. Then after getting the data, the profiler is
+ * stopped, and all profiler data is removed.
+ * @returns {Promise<Profile>}
+ */
+async function waitSamplingAndStopAndGetProfile() {
+ await Services.profiler.waitOnePeriodicSampling();
+ return stopNowAndGetProfile();
+}
+
+/**
+ * Verifies that a marker is an interval marker.
+ *
+ * @param {InflatedMarker} marker
+ * @returns {boolean}
+ */
+function isIntervalMarker(inflatedMarker) {
+ return (
+ inflatedMarker.phase === 1 &&
+ typeof inflatedMarker.startTime === "number" &&
+ typeof inflatedMarker.endTime === "number"
+ );
+}
+
+/**
+ * @param {Profile} profile
+ * @returns {Thread[]}
+ */
+function getThreads(profile) {
+ const threads = [];
+
+ function getThreadsRecursive(process) {
+ for (const thread of process.threads) {
+ threads.push(thread);
+ }
+ for (const subprocess of process.processes) {
+ getThreadsRecursive(subprocess);
+ }
+ }
+
+ getThreadsRecursive(profile);
+ return threads;
+}
+
+/**
+ * Find a specific marker schema from any process of a profile.
+ *
+ * @param {Profile} profile
+ * @param {string} name
+ * @returns {MarkerSchema}
+ */
+function getSchema(profile, name) {
+ {
+ const schema = profile.meta.markerSchema.find(s => s.name === name);
+ if (schema) {
+ return schema;
+ }
+ }
+ for (const subprocess of profile.processes) {
+ const schema = subprocess.meta.markerSchema.find(s => s.name === name);
+ if (schema) {
+ return schema;
+ }
+ }
+ console.error("Parent process schema", profile.meta.markerSchema);
+ for (const subprocess of profile.processes) {
+ console.error("Child process schema", subprocess.meta.markerSchema);
+ }
+ throw new Error(`Could not find a schema for "${name}".`);
+}
+
+/**
+ * This escapes all characters that have a special meaning in RegExps.
+ * This was stolen from https://github.com/sindresorhus/escape-string-regexp and
+ * so it is licence MIT and:
+ * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com).
+ * See the full license in https://raw.githubusercontent.com/sindresorhus/escape-string-regexp/main/license.
+ * @param {string} string The string to be escaped
+ * @returns {string} The result
+ */
+function escapeStringRegexp(string) {
+ if (typeof string !== "string") {
+ throw new TypeError("Expected a string");
+ }
+
+ // Escape characters with special meaning either inside or outside character
+ // sets. Use a simple backslash escape when it’s always valid, and a `\xnn`
+ // escape when the simpler form would be disallowed by Unicode patterns’
+ // stricter grammar.
+ return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
+}
+
+/** ------ Assertions helper ------ */
+/**
+ * This assert helper function makes it easy to check a lot of properties in an
+ * object. We augment Assert.sys.mjs to make it easier to use.
+ */
+Object.assign(Assert, {
+ /*
+ * It checks if the properties on the right are all present in the object on
+ * the left. Note that the object might still have other properties (see
+ * objectContainsOnly below if you want the stricter form).
+ *
+ * The basic form does basic equality on each expected property:
+ *
+ * Assert.objectContains(fixture, {
+ * foo: "foo",
+ * bar: 1,
+ * baz: true,
+ * });
+ *
+ * But it also has a more powerful form with expectations. The available
+ * expectations are:
+ * - any(): this only checks for the existence of the property, not its value
+ * - number(), string(), boolean(), bigint(), function(), symbol(), object():
+ * this checks if the value is of this type
+ * - objectContains(expected): this applies Assert.objectContains()
+ * recursively on this property.
+ * - stringContains(needle): this checks if the expected value is included in
+ * the property value.
+ * - stringMatches(regexp): this checks if the property value matches this
+ * regexp. The regexp can be passed as a string, to be dynamically built.
+ *
+ * example:
+ *
+ * Assert.objectContains(fixture, {
+ * name: Expect.stringMatches(`Load \\d+:.*${url}`),
+ * data: Expect.objectContains({
+ * status: "STATUS_STOP",
+ * URI: Expect.stringContains("https://"),
+ * requestMethod: "GET",
+ * contentType: Expect.string(),
+ * startTime: Expect.number(),
+ * cached: Expect.boolean(),
+ * }),
+ * });
+ *
+ * Each expectation will translate into one or more Assert call. Therefore if
+ * one expectation fails, this will be clearly visible in the test output.
+ *
+ * Expectations can also be normal functions, for example:
+ *
+ * Assert.objectContains(fixture, {
+ * number: value => Assert.greater(value, 5)
+ * });
+ *
+ * Note that you'll need to use Assert inside this function.
+ */
+ objectContains(object, expectedProperties) {
+ // Basic tests: we don't want to run other assertions if these tests fail.
+ if (typeof object !== "object") {
+ this.ok(
+ false,
+ `The first parameter should be an object, but found: ${object}.`
+ );
+ return;
+ }
+
+ if (typeof expectedProperties !== "object") {
+ this.ok(
+ false,
+ `The second parameter should be an object, but found: ${expectedProperties}.`
+ );
+ return;
+ }
+
+ for (const key of Object.keys(expectedProperties)) {
+ const expected = expectedProperties[key];
+ if (!(key in object)) {
+ this.report(
+ true,
+ object,
+ expectedProperties,
+ `The object should contain the property "${key}", but it's missing.`
+ );
+ continue;
+ }
+
+ if (typeof expected === "function") {
+ // This is a function, so let's call it.
+ expected(
+ object[key],
+ `The object should contain the property "${key}" with an expected value and type.`
+ );
+ } else {
+ // Otherwise, we check for equality.
+ this.equal(
+ object[key],
+ expectedProperties[key],
+ `The object should contain the property "${key}" with an expected value.`
+ );
+ }
+ }
+ },
+
+ /**
+ * This is very similar to the previous `objectContains`, but this also looks
+ * at the number of the objects' properties. Thus this will fail if the
+ * objects don't have the same properties exactly.
+ */
+ objectContainsOnly(object, expectedProperties) {
+ // Basic tests: we don't want to run other assertions if these tests fail.
+ if (typeof object !== "object") {
+ this.ok(
+ false,
+ `The first parameter should be an object but found: ${object}.`
+ );
+ return;
+ }
+
+ if (typeof expectedProperties !== "object") {
+ this.ok(
+ false,
+ `The second parameter should be an object but found: ${expectedProperties}.`
+ );
+ return;
+ }
+
+ // In objectContainsOnly, we specifically want to check if all properties
+ // from the fixture object are expected.
+ // We'll be failing a test only for the specific properties that weren't
+ // expected, and only fail with one message, so that the test outputs aren't
+ // spammed.
+ const extraProperties = [];
+ for (const fixtureKey of Object.keys(object)) {
+ if (!(fixtureKey in expectedProperties)) {
+ extraProperties.push(fixtureKey);
+ }
+ }
+
+ if (extraProperties.length) {
+ // Some extra properties have been found.
+ this.report(
+ true,
+ object,
+ expectedProperties,
+ `These properties are present, but shouldn't: "${extraProperties.join(
+ '", "'
+ )}".`
+ );
+ }
+
+ // Now, let's carry on the rest of our work.
+ this.objectContains(object, expectedProperties);
+ },
+});
+
+const Expect = {
+ any: () => actual => {} /* We don't check anything more than the presence of this property. */,
+};
+
+/* These functions are part of the Assert object, and we want to reuse them. */
+[
+ "stringContains",
+ "stringMatches",
+ "objectContains",
+ "objectContainsOnly",
+].forEach(
+ assertChecker =>
+ (Expect[assertChecker] = expected => (actual, ...moreArgs) =>
+ Assert[assertChecker](actual, expected, ...moreArgs))
+);
+
+/* These functions will only check for the type. */
+[
+ "number",
+ "string",
+ "boolean",
+ "bigint",
+ "symbol",
+ "object",
+ "function",
+].forEach(type => (Expect[type] = makeTypeChecker(type)));
+
+function makeTypeChecker(type) {
+ return (...unexpectedArgs) => {
+ if (unexpectedArgs.length) {
+ throw new Error(
+ "Type checkers expectations aren't expecting any argument."
+ );
+ }
+ return (actual, message) => {
+ const isCorrect = typeof actual === type;
+ Assert.report(!isCorrect, actual, type, message, "has type");
+ };
+ };
+}
+/* ------ End of assertion helper ------ */
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..c5bc264623
--- /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..47f6259047
--- /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"