/* 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/. */ "use strict"; // Recording already set preferences. const devtoolsPreferences = Services.prefs.getBranch("devtools"); const alreadySetPreferences = new Set(); for (const pref of devtoolsPreferences.getChildList("")) { if (devtoolsPreferences.prefHasUserValue(pref)) { alreadySetPreferences.add(pref); } } // Reset all devtools preferences on test end. registerCleanupFunction(async () => { await SpecialPowers.flushPrefEnv(); // Reset devtools preferences modified by the test. for (const pref of devtoolsPreferences.getChildList("")) { if ( devtoolsPreferences.prefHasUserValue(pref) && !alreadySetPreferences.has(pref) ) { devtoolsPreferences.clearUserPref(pref); } } }); // Ignore promise rejections for actions triggered after panels are closed. { const { PromiseTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PromiseTestUtils.sys.mjs" ); PromiseTestUtils.allowMatchingRejectionsGlobally( /REDUX_MIDDLEWARE_IGNORED_REDUX_ACTION/ ); } // Load the tracker very first in order to ensure tracking all objects created by DevTools. // This is especially important for allocation sites. We need to catch the global the // earliest possible in order to ensure that all allocation objects come with a stack. // // If we want to track DevTools module loader we should ensure loading Loader.sys.mjs within // the `testScript` Function. i.e. after having calling startRecordingAllocations. let tracker, releaseTrackerLoader; { const { useDistinctSystemPrincipalLoader, releaseDistinctSystemPrincipalLoader, } = ChromeUtils.importESModule( "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", { global: "shared" } ); const requester = {}; const loader = useDistinctSystemPrincipalLoader(requester); releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester); const { allocationTracker } = loader.require( "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" ); tracker = allocationTracker({ watchDevToolsGlobals: true }); } // /!\ Be careful about imports/require // // Some tests may record the very first time we load a module. // If we start loading them from here, we might only retrieve the already loaded // module from the loader's cache. This would no longer highlight the cost // of loading a new module from scratch. // // => Avoid loading devtools module as much as possible // => If you really have to, lazy load them ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => { return ChromeUtils.importESModule( "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" ); }); ChromeUtils.defineLazyGetter(this, "TraceObjects", () => { return ChromeUtils.importESModule( "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs" ); }); // So that PERFHERDER data can be extracted from the logs. SimpleTest.requestCompleteLog(); // We have to disable testing mode, or various debug instructions are enabled. // We especially want to disable redux store history, which would leak all the actions! SpecialPowers.pushPrefEnv({ set: [["devtools.testing", false]], }); // Set DEBUG_DEVTOOLS_ALLOCATIONS=allocations|leaks in order print debug informations. const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS"); async function addTab(url) { const tab = BrowserTestUtils.addTab(gBrowser, url); gBrowser.selectedTab = tab; await BrowserTestUtils.browserLoaded(tab.linkedBrowser); return tab; } /** * This function will force some garbage collection before recording * data about allocated objects. * * This accept an optional boolean to also record the content process objects * of the current tab. That, in addition of objects from the parent process, * which are always recorded. * * This return same data object which is meant to be passed to `stopRecordingAllocations` as-is. * * See README.md file in this folder. */ async function startRecordingAllocations({ alsoRecordContentProcess = false, } = {}) { // Also start recording allocations in the content process, if requested if (alsoRecordContentProcess) { await SpecialPowers.spawn( gBrowser.selectedBrowser, [DEBUG_ALLOCATIONS], async debug_allocations => { const { DevToolsLoader } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); const { useDistinctSystemPrincipalLoader, releaseDistinctSystemPrincipalLoader, } = ChromeUtils.importESModule( "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" ); const requester = {}; const loader = useDistinctSystemPrincipalLoader(requester); const { allocationTracker } = loader.require( "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" ); // We watch all globals in the content process, (instead of only DevTools global in parent process) // because we may easily leak web page objects, which aren't in DevTools global. const tracker = allocationTracker({ watchAllGlobals: true }); // /!\ HACK: store tracker and releaseTrackerLoader on DevToolsLoader in order // to be able to reuse them in a following call to SpecialPowers.spawn DevToolsLoader.tracker = tracker; DevToolsLoader.releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester); await tracker.startRecordingAllocations(debug_allocations); } ); // Trigger a GC in the parent process as this additional ContentTask // seems to make harder to release objects created before we start recording. await tracker.doGC(); } await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS); } /** * See doc of startRecordingAllocations */ async function stopRecordingAllocations( recordName, { alsoRecordContentProcess = false } = {} ) { // Ensure that Memory API didn't ran out of buffers ok(!tracker.overflowed, "Allocation were all recorded in the parent process"); // And finally, retrieve the record *after* having ran the test const parentProcessData = await tracker.stopRecordingAllocations(DEBUG_ALLOCATIONS); const leakedObjects = TrackedObjects.getStillAllocatedObjects(); if (leakedObjects.length) { await TraceObjects.traceObjects(leakedObjects, tracker.getSnapshotFile()); } let contentProcessData = null; if (alsoRecordContentProcess) { contentProcessData = await SpecialPowers.spawn( gBrowser.selectedBrowser, [DEBUG_ALLOCATIONS], debug_allocations => { const { DevToolsLoader } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); const { tracker } = DevToolsLoader; ok( !tracker.overflowed, "Allocation were all recorded in the content process" ); return tracker.stopRecordingAllocations(debug_allocations); } ); } const trackedObjectsInContent = await SpecialPowers.spawn( gBrowser.selectedBrowser, [], async () => { const TrackedObjects = ChromeUtils.importESModule( "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" ); const leakedObjects = TrackedObjects.getStillAllocatedObjects(); if (leakedObjects.length) { const TraceObjects = ChromeUtils.importESModule( "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs" ); // Only pass 'weakRef' as Memory API and 'ubiNodeId' can only be inspected in the parent process await TraceObjects.traceObjects( leakedObjects.map(e => { return { weakRef: e.weakRef, }; }) ); const { DevToolsLoader } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); const { tracker } = DevToolsLoader; // Record the heap snapshot from the content process, // and pass the record's filepath to the parent process // As only the parent process can read the file because // of sandbox restrictions made to content processes regarding file I/O. const snapshotFile = tracker.getSnapshotFile(); return { snapshotFile, // Only pass ubi::Node::Id from this content process to the parent process. // `leakedObjects`'s `weakRef` attributes can't be transferred across processes. // TraceObjects.traceObjects in the parent process will only log leaks // via the Memory API (and Node Id's). objectUbiNodeIds: leakedObjects.map(e => { return { ubiNodeId: e.ubiNodeId, }; }), }; } return null; } ); if (trackedObjectsInContent) { TraceObjects.traceObjects( trackedObjectsInContent.objectUbiNodeIds, trackedObjectsInContent.snapshotFile ); } // Craft the JSON object required to save data in talos database info( `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process` ); const PERFHERDER_DATA = { framework: { name: "devtools", }, suites: [ { name: recordName + ":parent-process", subtests: [ { name: "objects-with-stacks", value: parentProcessData.objectsWithStack, }, { name: "memory", value: parentProcessData.memory, }, ], }, ], }; if (alsoRecordContentProcess) { info( `The ${recordName} test leaked ${contentProcessData.objectsWithStack} objects (${contentProcessData.objectsWithoutStack} with missing allocation site) in the content process` ); PERFHERDER_DATA.suites.push({ name: recordName + ":content-process", subtests: [ { name: "objects-with-stacks", value: contentProcessData.objectsWithStack, }, { name: "memory", value: contentProcessData.memory, }, ], }); // Finally release the tracker loader in content process. await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { const { DevToolsLoader } = ChromeUtils.importESModule( "resource://devtools/shared/loader/Loader.sys.mjs" ); DevToolsLoader.releaseTrackerLoader(); }); } // And release the tracker loader in the parent process releaseTrackerLoader(); // Log it to stdout so that perfherder can collect this data. // This only works if we called `SimpleTest.requestCompleteLog()`! info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA)); }