StartupRecorder.sys.mjs (7752B)
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 const Cm = Components.manager; 6 Cm.QueryInterface(Ci.nsIServiceManager); 7 8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 10 11 const lazy = {}; 12 13 XPCOMUtils.defineLazyPreferenceGetter( 14 lazy, 15 "BROWSER_STARTUP_RECORD", 16 "browser.startup.record", 17 false 18 ); 19 20 XPCOMUtils.defineLazyPreferenceGetter( 21 lazy, 22 "BROWSER_STARTUP_RECORD_IMAGES", 23 "browser.startup.recordImages", 24 false 25 ); 26 27 let firstPaintNotification = "xul-window-visible"; 28 // On Linux widget-first-paint fires much later than expected and 29 // xul-window-visible fires too early for currently unknown reasons. 30 if (AppConstants.platform == "linux") { 31 firstPaintNotification = "document-shown"; 32 } 33 34 let win, canvas; 35 let paints = []; 36 let afterPaintListener = () => { 37 let startTime = ChromeUtils.now(); 38 let width, height; 39 canvas.width = width = win.innerWidth; 40 canvas.height = height = win.innerHeight; 41 if (width < 1 || height < 1) { 42 return; 43 } 44 let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true }); 45 46 ctx.drawWindow( 47 win, 48 0, 49 0, 50 width, 51 height, 52 "white", 53 ctx.DRAWWINDOW_DO_NOT_FLUSH | 54 ctx.DRAWWINDOW_DRAW_VIEW | 55 ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | 56 ctx.DRAWWINDOW_USE_WIDGET_LAYERS 57 ); 58 paints.push({ 59 data: ctx.getImageData(0, 0, width, height).data, 60 width, 61 height, 62 }); 63 ChromeUtils.addProfilerMarker( 64 "startupRecorder", 65 { category: "Test", startTime }, 66 `screenshot: ${width}x${height}px` 67 ); 68 }; 69 70 /** 71 * The StartupRecorder component observes notifications at various stages of 72 * startup and records the set of JS modules that were already loaded at 73 * each of these points. 74 * The records are meant to be used by startup tests in 75 * browser/base/content/test/performance 76 * This component only exists in nightly and debug builds, it doesn't ship in 77 * our release builds. 78 */ 79 export function StartupRecorder() { 80 this.wrappedJSObject = this; 81 this.data = { 82 images: { 83 "image-drawing": new Set(), 84 "image-loading": new Set(), 85 }, 86 code: {}, 87 prefStats: {}, 88 }; 89 this.done = new Promise(resolve => { 90 this._resolve = resolve; 91 }); 92 } 93 94 StartupRecorder.prototype = { 95 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 96 97 record(name) { 98 ChromeUtils.addProfilerMarker( 99 "startupRecorder", 100 { category: "Test" }, 101 name 102 ); 103 this.data.code[name] = { 104 modules: Cu.loadedESModules, 105 services: Object.keys(Cc).filter(c => { 106 try { 107 return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports); 108 } catch (e) { 109 return false; 110 } 111 }), 112 }; 113 }, 114 115 observe(subject, topic, data) { 116 if (topic == "app-startup" || topic == "content-process-ready-for-script") { 117 // Don't do anything in xpcshell. 118 if (Services.appinfo.ID != "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { 119 return; 120 } 121 122 if (!lazy.BROWSER_STARTUP_RECORD && !lazy.BROWSER_STARTUP_RECORD_IMAGES) { 123 this._resolve(); 124 this._resolve = null; 125 return; 126 } 127 128 // We can't ensure our observer will be called first or last, so the list of 129 // topics we observe here should avoid the topics used to trigger things 130 // during startup (eg. the topics observed by BrowserGlue.sys.mjs). 131 let topics = [ 132 "profile-do-change", // This catches stuff loaded during app-startup 133 "toplevel-window-ready", // Catches stuff from final-ui-startup 134 firstPaintNotification, 135 "sessionstore-windows-restored", 136 "browser-startup-idle-tasks-finished", 137 ]; 138 139 if (lazy.BROWSER_STARTUP_RECORD_IMAGES) { 140 // For code simplicify, recording images excludes the other startup 141 // recorder behaviors, so we can observe only the image topics. 142 topics = [ 143 "image-loading", 144 "image-drawing", 145 "browser-startup-idle-tasks-finished", 146 ]; 147 } 148 for (let t of topics) { 149 Services.obs.addObserver(this, t); 150 } 151 return; 152 } 153 154 // We only care about the first paint notification for browser windows, and 155 // not other types (for example, the gfx sanity test window) 156 if (topic == firstPaintNotification) { 157 // In the case we're handling xul-window-visible, we'll have been handed 158 // an nsIAppWindow instead of an nsIDOMWindow. 159 if (subject instanceof Ci.nsIAppWindow) { 160 subject = subject 161 .QueryInterface(Ci.nsIInterfaceRequestor) 162 .getInterface(Ci.nsIDOMWindow); 163 } 164 165 // In the case we're handling document-shown, we'll have been handed 166 // an HTMLDocument instead of an nsIDOMWindow. 167 let doc = topic == "document-shown" ? subject : subject.document; 168 169 if ( 170 doc.documentElement.getAttribute("windowtype") != "navigator:browser" 171 ) { 172 return; 173 } 174 } 175 176 if (topic == "image-drawing" || topic == "image-loading") { 177 this.data.images[topic].add(data); 178 return; 179 } 180 181 Services.obs.removeObserver(this, topic); 182 183 if (topic == firstPaintNotification) { 184 // Because of the check for navigator:browser we made earlier, we know 185 // that if we got here, then the subject must be the first browser window. 186 win = topic == "document-shown" ? subject.defaultView : subject; 187 canvas = win.document.createElementNS( 188 "http://www.w3.org/1999/xhtml", 189 "canvas" 190 ); 191 canvas.mozOpaque = true; 192 afterPaintListener(); 193 win.addEventListener("MozAfterPaint", afterPaintListener); 194 } 195 196 if (topic == "sessionstore-windows-restored") { 197 // We use idleDispatchToMainThread here to record the set of 198 // loaded scripts after we are fully done with startup and ready 199 // to react to user events. 200 Services.tm.dispatchToMainThread( 201 this.record.bind(this, "before handling user events") 202 ); 203 } else if (topic == "browser-startup-idle-tasks-finished") { 204 if (lazy.BROWSER_STARTUP_RECORD_IMAGES) { 205 Services.obs.removeObserver(this, "image-drawing"); 206 Services.obs.removeObserver(this, "image-loading"); 207 this._resolve(); 208 this._resolve = null; 209 return; 210 } 211 212 this.record("before becoming idle"); 213 win.removeEventListener("MozAfterPaint", afterPaintListener); 214 win = null; 215 this.data.frames = paints; 216 this.data.prefStats = {}; 217 if (AppConstants.DEBUG) { 218 Services.prefs.readStats( 219 (key, value) => (this.data.prefStats[key] = value) 220 ); 221 } 222 paints = null; 223 224 if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) { 225 this._resolve(); 226 this._resolve = null; 227 return; 228 } 229 230 Services.profiler.getProfileDataAsync().then(profileData => { 231 this.data.profile = profileData; 232 // There's no equivalent StartProfiler call in this file because the 233 // profiler is started using the MOZ_PROFILER_STARTUP environment 234 // variable in browser/base/content/test/performance/browser.toml 235 Services.profiler.StopProfiler(); 236 237 this._resolve(); 238 this._resolve = null; 239 }); 240 } else { 241 const topicsToNames = { 242 "profile-do-change": "before profile selection", 243 "toplevel-window-ready": "before opening first browser window", 244 }; 245 topicsToNames[firstPaintNotification] = "before first paint"; 246 this.record(topicsToNames[topic]); 247 } 248 }, 249 };