summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js')
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js470
1 files changed, 470 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
new file mode 100644
index 0000000000..a10021e088
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
@@ -0,0 +1,470 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const HANG_TIME = 1000; // ms
+const TEST_USER_INTERACTION_ID = "testing.interaction";
+const TEST_CLOBBERED_USER_INTERACTION_ID = `${TEST_USER_INTERACTION_ID} (clobbered)`;
+const TEST_VALUE_1 = "some value";
+const TEST_VALUE_2 = "some other value";
+const TEST_ADDITIONAL_TEXT_1 = "some additional text";
+const TEST_ADDITIONAL_TEXT_2 = "some other additional text";
+
+/**
+ * Intentionally hangs the main thread in the parent process for
+ * HANG_TIME, and then returns the BHR hang report generated for
+ * that hang.
+ *
+ * @returns {Promise}
+ * @resolves {nsIHangDetails}
+ * The hang report that was created.
+ */
+async function hangAndWaitForReport(expectTestAnnotation) {
+ let hangPromise = TestUtils.topicObserved("bhr-thread-hang", subject => {
+ let hang = subject.QueryInterface(Ci.nsIHangDetails);
+ if (hang.thread != "Gecko") {
+ return false;
+ }
+
+ if (expectTestAnnotation) {
+ return hang.annotations.some(annotation =>
+ annotation[0].startsWith(TEST_USER_INTERACTION_ID)
+ );
+ }
+
+ return hang.annotations.every(
+ annotation => annotation[0] != TEST_USER_INTERACTION_ID
+ );
+ });
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < HANG_TIME) {}
+ });
+
+ let [report] = await hangPromise;
+ return report;
+}
+
+/**
+ * Makes sure that the profiler is initialized. This has the added side-effect
+ * of making sure that BHR is initialized as well.
+ */
+function ensureProfilerInitialized() {
+ startProfiler();
+ stopProfiler();
+}
+
+function stopProfiler() {
+ Services.profiler.StopProfiler();
+}
+
+function startProfiler() {
+ // Starting and stopping the profiler with the "stackwalk" flag will cause the
+ // profiler's stackwalking features to be synchronously initialized. This
+ // should prevent us from not initializing BHR quickly enough.
+ Services.profiler.StartProfiler(1000, 10, ["stackwalk"]);
+}
+
+/**
+ * Given a performance profile object, returns a count of how many
+ * markers matched the value (and optional additionalText) that
+ * the UserInteraction backend added. This function only checks
+ * markers on thread 0.
+ *
+ * @param {Object} profile
+ * A profile returned from Services.profiler.getProfileData();
+ * @param {String} value
+ * The value that the marker is expected to have.
+ * @param {String} additionalText
+ * (Optional) If additionalText was provided when finishing the
+ * UserInteraction, then markerCount will check for a marker with
+ * text in the form of "value,additionalText".
+ * @returns {Number}
+ * A count of how many markers appear that match the criteria.
+ */
+function markerCount(profile, value, additionalText) {
+ let expectedName = value;
+ if (additionalText) {
+ expectedName = [value, additionalText].join(",");
+ }
+
+ let thread0 = profile.threads[0];
+ let stringTable = thread0.stringTable;
+ let markerStringIndex = stringTable.indexOf(TEST_USER_INTERACTION_ID);
+
+ let markers = thread0.markers.data.filter(markerData => {
+ return (
+ markerData[0] == markerStringIndex && markerData[5].name == expectedName
+ );
+ });
+
+ return markers.length;
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_USER_INTERACTION_ID name, and the passed value.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return annotation[0] == TEST_USER_INTERACTION_ID && annotation[1] == value;
+ });
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_CLOBBERED_USER_INTERACTION_ID name, and the passed value.
+ *
+ * This check should be used when we expect a pre-existing UserInteraction to
+ * have been clobbered by a new UserInteraction.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasClobberedHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return (
+ annotation[0] == TEST_CLOBBERED_USER_INTERACTION_ID &&
+ annotation[1] == value
+ );
+ });
+}
+
+/**
+ * Tests that UserInteractions cause BHR annotations and profiler
+ * markers to be written.
+ */
+add_task(async function test_recording_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+ stopProfiler();
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 1,
+ "Should have found the marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the BHR annotation set."
+ );
+
+ // Next, we'll make sure that when we're not running a UserInteraction,
+ // no marker or annotation is set.
+ startProfiler();
+
+ report = await hangAndWaitForReport(false);
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not find the marker in the profile."
+ );
+ Assert.ok(
+ !hasHangAnnotation(report),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can set multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be updated, resulting in their BHR
+ * annotations and profiler markers to also be updated.
+ */
+add_task(async function test_updating_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Updating the UserInteraction means that a new value will overwrite
+ // the old.
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the original marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 1,
+ "Should have found the updated marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the updated BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can update multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ // Now swap the values between the two UserInteractions
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1);
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2);
+
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be cancelled, resulting in no BHR
+ * annotations and profiler markers being recorded.
+ */
+add_task(async function test_cancelling_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID);
+ let report = await hangAndWaitForReport(false);
+
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can cancel multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj2);
+
+ report = await hangAndWaitForReport(false);
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 0,
+ "Should not have found the second marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_2),
+ "Should not have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that starting UserInteractions with the same ID and object
+ * creates a clobber annotation.
+ */
+add_task(async function test_clobbered_annotations() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Now clobber the original UserInteraction
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+
+ let report = await hangAndWaitForReport(true);
+ Assert.ok(
+ UserInteraction.finish(TEST_USER_INTERACTION_ID),
+ "Should have been able to finish the UserInteraction."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasClobberedHangAnnotation(report, TEST_VALUE_2),
+ "Should have the clobber BHR annotation set."
+ );
+});