/* 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." ); });