head.js (11174B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // Recording already set preferences. 8 const devtoolsPreferences = Services.prefs.getBranch("devtools"); 9 const alreadySetPreferences = new Set(); 10 for (const pref of devtoolsPreferences.getChildList("")) { 11 if (devtoolsPreferences.prefHasUserValue(pref)) { 12 alreadySetPreferences.add(pref); 13 } 14 } 15 16 // Reset all devtools preferences on test end. 17 registerCleanupFunction(async () => { 18 await SpecialPowers.flushPrefEnv(); 19 20 // Reset devtools preferences modified by the test. 21 for (const pref of devtoolsPreferences.getChildList("")) { 22 if ( 23 devtoolsPreferences.prefHasUserValue(pref) && 24 !alreadySetPreferences.has(pref) 25 ) { 26 devtoolsPreferences.clearUserPref(pref); 27 } 28 } 29 }); 30 31 // Ignore promise rejections for actions triggered after panels are closed. 32 { 33 const { PromiseTestUtils } = ChromeUtils.importESModule( 34 "resource://testing-common/PromiseTestUtils.sys.mjs" 35 ); 36 PromiseTestUtils.allowMatchingRejectionsGlobally( 37 /REDUX_MIDDLEWARE_IGNORED_REDUX_ACTION/ 38 ); 39 } 40 41 // Load the tracker very first in order to ensure tracking all objects created by DevTools. 42 // This is especially important for allocation sites. We need to catch the global the 43 // earliest possible in order to ensure that all allocation objects come with a stack. 44 // 45 // If we want to track DevTools module loader we should ensure loading Loader.sys.mjs within 46 // the `testScript` Function. i.e. after having calling startRecordingAllocations. 47 let tracker, releaseTrackerLoader; 48 { 49 const { 50 useDistinctSystemPrincipalLoader, 51 releaseDistinctSystemPrincipalLoader, 52 } = ChromeUtils.importESModule( 53 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", 54 { global: "shared" } 55 ); 56 57 const requester = {}; 58 const loader = useDistinctSystemPrincipalLoader(requester); 59 releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester); 60 const { allocationTracker } = loader.require( 61 "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" 62 ); 63 tracker = allocationTracker({ watchDevToolsGlobals: true }); 64 } 65 66 // /!\ Be careful about imports/require 67 // 68 // Some tests may record the very first time we load a module. 69 // If we start loading them from here, we might only retrieve the already loaded 70 // module from the loader's cache. This would no longer highlight the cost 71 // of loading a new module from scratch. 72 // 73 // => Avoid loading devtools module as much as possible 74 // => If you really have to, lazy load them 75 76 ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => { 77 return ChromeUtils.importESModule( 78 "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" 79 ); 80 }); 81 82 ChromeUtils.defineLazyGetter(this, "TraceObjects", () => { 83 return ChromeUtils.importESModule( 84 "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs" 85 ); 86 }); 87 88 // So that PERFHERDER data can be extracted from the logs. 89 SimpleTest.requestCompleteLog(); 90 91 // We have to disable testing mode, or various debug instructions are enabled. 92 // We especially want to disable redux store history, which would leak all the actions! 93 SpecialPowers.pushPrefEnv({ 94 set: [["devtools.testing", false]], 95 }); 96 97 // Set DEBUG_DEVTOOLS_ALLOCATIONS=allocations|leaks in order print debug informations. 98 const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS"); 99 100 async function addTab(url) { 101 const tab = BrowserTestUtils.addTab(gBrowser, url); 102 gBrowser.selectedTab = tab; 103 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 104 return tab; 105 } 106 107 /** 108 * This function will force some garbage collection before recording 109 * data about allocated objects. 110 * 111 * This accept an optional boolean to also record the content process objects 112 * of the current tab. That, in addition of objects from the parent process, 113 * which are always recorded. 114 * 115 * This return same data object which is meant to be passed to `stopRecordingAllocations` as-is. 116 * 117 * See README.md file in this folder. 118 */ 119 async function startRecordingAllocations({ 120 alsoRecordContentProcess = false, 121 } = {}) { 122 // Also start recording allocations in the content process, if requested 123 if (alsoRecordContentProcess) { 124 await SpecialPowers.spawn( 125 gBrowser.selectedBrowser, 126 [DEBUG_ALLOCATIONS], 127 async debug_allocations => { 128 const { DevToolsLoader } = ChromeUtils.importESModule( 129 "resource://devtools/shared/loader/Loader.sys.mjs" 130 ); 131 132 const { 133 useDistinctSystemPrincipalLoader, 134 releaseDistinctSystemPrincipalLoader, 135 } = ChromeUtils.importESModule( 136 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" 137 ); 138 139 const requester = {}; 140 const loader = useDistinctSystemPrincipalLoader(requester); 141 const { allocationTracker } = loader.require( 142 "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" 143 ); 144 // We watch all globals in the content process, (instead of only DevTools global in parent process) 145 // because we may easily leak web page objects, which aren't in DevTools global. 146 const tracker = allocationTracker({ watchAllGlobals: true }); 147 148 // /!\ HACK: store tracker and releaseTrackerLoader on DevToolsLoader in order 149 // to be able to reuse them in a following call to SpecialPowers.spawn 150 DevToolsLoader.tracker = tracker; 151 DevToolsLoader.releaseTrackerLoader = () => 152 releaseDistinctSystemPrincipalLoader(requester); 153 154 await tracker.startRecordingAllocations(debug_allocations); 155 } 156 ); 157 // Trigger a GC in the parent process as this additional ContentTask 158 // seems to make harder to release objects created before we start recording. 159 await tracker.doGC(); 160 } 161 162 await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS); 163 } 164 165 /** 166 * See doc of startRecordingAllocations 167 */ 168 async function stopRecordingAllocations( 169 recordName, 170 { alsoRecordContentProcess = false } = {} 171 ) { 172 // Ensure that Memory API didn't ran out of buffers 173 ok(!tracker.overflowed, "Allocation were all recorded in the parent process"); 174 175 // And finally, retrieve the record *after* having ran the test 176 const parentProcessData = 177 await tracker.stopRecordingAllocations(DEBUG_ALLOCATIONS); 178 179 const leakedObjects = TrackedObjects.getStillAllocatedObjects(); 180 if (leakedObjects.length) { 181 await TraceObjects.traceObjects(leakedObjects, tracker.getSnapshotFile()); 182 } 183 184 let contentProcessData = null; 185 if (alsoRecordContentProcess) { 186 contentProcessData = await SpecialPowers.spawn( 187 gBrowser.selectedBrowser, 188 [DEBUG_ALLOCATIONS], 189 debug_allocations => { 190 const { DevToolsLoader } = ChromeUtils.importESModule( 191 "resource://devtools/shared/loader/Loader.sys.mjs" 192 ); 193 const { tracker } = DevToolsLoader; 194 ok( 195 !tracker.overflowed, 196 "Allocation were all recorded in the content process" 197 ); 198 return tracker.stopRecordingAllocations(debug_allocations); 199 } 200 ); 201 } 202 203 const trackedObjectsInContent = await SpecialPowers.spawn( 204 gBrowser.selectedBrowser, 205 [], 206 async () => { 207 const TrackedObjects = ChromeUtils.importESModule( 208 "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" 209 ); 210 const leakedObjects = TrackedObjects.getStillAllocatedObjects(); 211 if (leakedObjects.length) { 212 const TraceObjects = ChromeUtils.importESModule( 213 "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs" 214 ); 215 // Only pass 'weakRef' as Memory API and 'ubiNodeId' can only be inspected in the parent process 216 await TraceObjects.traceObjects( 217 leakedObjects.map(e => { 218 return { 219 weakRef: e.weakRef, 220 }; 221 }) 222 ); 223 const { DevToolsLoader } = ChromeUtils.importESModule( 224 "resource://devtools/shared/loader/Loader.sys.mjs" 225 ); 226 const { tracker } = DevToolsLoader; 227 // Record the heap snapshot from the content process, 228 // and pass the record's filepath to the parent process 229 // As only the parent process can read the file because 230 // of sandbox restrictions made to content processes regarding file I/O. 231 const snapshotFile = tracker.getSnapshotFile(); 232 return { 233 snapshotFile, 234 // Only pass ubi::Node::Id from this content process to the parent process. 235 // `leakedObjects`'s `weakRef` attributes can't be transferred across processes. 236 // TraceObjects.traceObjects in the parent process will only log leaks 237 // via the Memory API (and Node Id's). 238 objectUbiNodeIds: leakedObjects.map(e => { 239 return { 240 ubiNodeId: e.ubiNodeId, 241 }; 242 }), 243 }; 244 } 245 return null; 246 } 247 ); 248 if (trackedObjectsInContent) { 249 TraceObjects.traceObjects( 250 trackedObjectsInContent.objectUbiNodeIds, 251 trackedObjectsInContent.snapshotFile 252 ); 253 } 254 255 // Craft the JSON object required to save data in talos database 256 info( 257 `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process` 258 ); 259 const PERFHERDER_DATA = { 260 framework: { 261 name: "devtools", 262 }, 263 suites: [ 264 { 265 name: recordName + ":parent-process", 266 subtests: [ 267 { 268 name: "objects-with-stacks", 269 value: parentProcessData.objectsWithStack, 270 }, 271 { 272 name: "memory", 273 value: parentProcessData.memory, 274 }, 275 ], 276 }, 277 ], 278 }; 279 if (alsoRecordContentProcess) { 280 info( 281 `The ${recordName} test leaked ${contentProcessData.objectsWithStack} objects (${contentProcessData.objectsWithoutStack} with missing allocation site) in the content process` 282 ); 283 PERFHERDER_DATA.suites.push({ 284 name: recordName + ":content-process", 285 subtests: [ 286 { 287 name: "objects-with-stacks", 288 value: contentProcessData.objectsWithStack, 289 }, 290 { 291 name: "memory", 292 value: contentProcessData.memory, 293 }, 294 ], 295 }); 296 297 // Finally release the tracker loader in content process. 298 await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { 299 const { DevToolsLoader } = ChromeUtils.importESModule( 300 "resource://devtools/shared/loader/Loader.sys.mjs" 301 ); 302 DevToolsLoader.releaseTrackerLoader(); 303 }); 304 } 305 306 // And release the tracker loader in the parent process 307 releaseTrackerLoader(); 308 309 // Log it to stdout so that perfherder can collect this data. 310 // This only works if we called `SimpleTest.requestCompleteLog()`! 311 info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA)); 312 }