perf.js (8860B)
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 /** 8 * @typedef {import("perf").BulkReceiving} BulkSending 9 */ 10 11 const { Actor } = require("resource://devtools/shared/protocol.js"); 12 const { perfSpec } = require("resource://devtools/shared/specs/perf.js"); 13 14 ChromeUtils.defineESModuleGetters( 15 this, 16 { 17 RecordingUtils: 18 "resource://devtools/shared/performance-new/recording-utils.sys.mjs", 19 Symbolication: 20 "resource://devtools/shared/performance-new/symbolication.sys.mjs", 21 }, 22 { global: "contextual" } 23 ); 24 25 // Some platforms are built without the Gecko Profiler. 26 const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci; 27 28 /** 29 * The PerfActor wraps the Gecko Profiler interface (aka Services.profiler). 30 */ 31 exports.PerfActor = class PerfActor extends Actor { 32 /** 33 * This counter is incremented at each new capture. This makes sure that the 34 * profile data and the additionalInformation are in sync. 35 * 36 * @type {number} 37 */ 38 #captureHandleCounter = 0; 39 40 /** 41 * This stores the profile data retrieved from the last call to 42 * startCaptureAndStopProfiler. 43 * 44 * @type {Promise<ArrayBuffer> |null} 45 */ 46 #previouslyRetrievedProfileDataPromise = null; 47 48 /** 49 * This stores the additionalInformation returned by 50 * getProfileDataAsGzippedArrayBufferThenStop so that it can be sent to the 51 * front using getPreviouslyRetrievedAdditionalInformation. 52 * 53 * @type {Promise<MockedExports.ProfileGenerationAdditionalInformation>| null} 54 */ 55 #previouslyRetrievedAdditionalInformationPromise = null; 56 57 constructor(conn) { 58 super(conn, perfSpec); 59 60 // Only setup the observers on a supported platform. 61 if (IS_SUPPORTED_PLATFORM) { 62 this._observer = { 63 observe: this._observe.bind(this), 64 }; 65 Services.obs.addObserver(this._observer, "profiler-started"); 66 Services.obs.addObserver(this._observer, "profiler-stopped"); 67 } 68 } 69 70 destroy() { 71 super.destroy(); 72 73 if (!IS_SUPPORTED_PLATFORM) { 74 return; 75 } 76 Services.obs.removeObserver(this._observer, "profiler-started"); 77 Services.obs.removeObserver(this._observer, "profiler-stopped"); 78 } 79 80 startProfiler(options) { 81 if (!IS_SUPPORTED_PLATFORM) { 82 return false; 83 } 84 85 // For a quick implementation, decide on some default values. These may need 86 // to be tweaked or made configurable as needed. 87 const settings = { 88 entries: options.entries || 1000000, 89 duration: options.duration || 0, 90 interval: options.interval || 1, 91 features: options.features || ["js", "stackwalk", "cpu", "memory"], 92 threads: options.threads || ["GeckoMain", "Compositor"], 93 activeTabID: RecordingUtils.getActiveBrowserID(), 94 }; 95 96 try { 97 // This can throw an error if the profiler is in the wrong state. 98 Services.profiler.StartProfiler( 99 settings.entries, 100 settings.interval, 101 settings.features, 102 settings.threads, 103 settings.activeTabID, 104 settings.duration 105 ); 106 } catch (e) { 107 // In case any errors get triggered, bailout with a false. 108 return false; 109 } 110 111 return true; 112 } 113 114 stopProfilerAndDiscardProfile() { 115 if (!IS_SUPPORTED_PLATFORM) { 116 return null; 117 } 118 return Services.profiler.StopProfiler(); 119 } 120 121 /** 122 * @type {string} debugPath 123 * @type {string} breakpadId 124 * @returns {Promise<[number[], number[], number[]]>} 125 */ 126 async getSymbolTable(debugPath, breakpadId) { 127 const libraries = Services.profiler.sharedLibraries; 128 const symbolicationService = Symbolication.createLocalSymbolicationService( 129 libraries, 130 [] 131 ); 132 const debugName = libraries.find( 133 lib => lib.path === debugPath && lib.breakpadId === breakpadId 134 )?.debugName; 135 136 if (debugName === undefined) { 137 throw new Error( 138 `Couldn't find the library with path ${debugPath} and breakpadId ${breakpadId}` 139 ); 140 } 141 142 const [addr, index, buffer] = await symbolicationService.getSymbolTable( 143 debugName, 144 breakpadId 145 ); 146 // The protocol does not support the transfer of typed arrays, so we convert 147 // these typed arrays to plain JS arrays of numbers now. 148 // Our return value type is declared as "array:array:number". 149 return [Array.from(addr), Array.from(index), Array.from(buffer)]; 150 } 151 152 async startCaptureAndStopProfiler() { 153 if (!IS_SUPPORTED_PLATFORM) { 154 throw new Error("Profiling is not supported on this platform."); 155 } 156 157 const capturePromise = 158 RecordingUtils.getProfileDataAsGzippedArrayBufferThenStop(); 159 160 this.#previouslyRetrievedProfileDataPromise = capturePromise.then( 161 ({ profileCaptureResult }) => { 162 if (profileCaptureResult.type === "ERROR") { 163 throw profileCaptureResult.error; 164 } 165 166 return profileCaptureResult.profile; 167 } 168 ); 169 170 this.#previouslyRetrievedAdditionalInformationPromise = capturePromise.then( 171 ({ additionalInformation }) => additionalInformation 172 ); 173 174 return ++this.#captureHandleCounter; 175 } 176 177 /** 178 * This actor function returns the profile data using the bulk protocol. 179 * 180 * @param {number} handle returned by startCaptureAndStopProfiler 181 * @returns {Promise<void>} 182 */ 183 async getPreviouslyCapturedProfileDataBulk(handle, startBulkSend) { 184 if (handle < this.#captureHandleCounter) { 185 // This handle is outdated, write a message to the console and throw an error 186 console.error( 187 `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.` 188 ); 189 throw new Error(`The requested data was not found.`); 190 } 191 192 if (this.#previouslyRetrievedProfileDataPromise === null) { 193 // No capture operation has been started, write a message and throw an error. 194 console.error( 195 `[devtools perf actor] In getPreviouslyCapturedProfileDataBulk, there's no data to be returned.` 196 ); 197 throw new Error(`The requested data was not found.`); 198 } 199 200 // Note that this promise might be rejected if there was an error. That's OK 201 // and part of the design. 202 const profile = await this.#previouslyRetrievedProfileDataPromise; 203 this.#previouslyRetrievedProfileDataPromise = null; 204 205 const bulk = await startBulkSend(profile.byteLength); 206 await bulk.copyFromBuffer(profile); 207 } 208 209 /** 210 * @param {number} handle returned by startCaptureAndStopProfiler 211 * @returns {Promise<MockedExports.ProfileGenerationAdditionalInformation>} 212 */ 213 async getPreviouslyRetrievedAdditionalInformation(handle) { 214 if (handle < this.#captureHandleCounter) { 215 // This handle is outdated, write a message to the console and throw an error 216 console.error( 217 `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, the requested handle ${handle} is smaller than the current counter ${this.#captureHandleCounter}.` 218 ); 219 throw new Error(`The requested data was not found.`); 220 } 221 222 if (this.#previouslyRetrievedAdditionalInformationPromise === null) { 223 // No capture operation has been started, write a message and throw an error. 224 console.error( 225 `[devtools perf actor] In getPreviouslyRetrievedAdditionalInformation, there's no data to be returned.` 226 ); 227 throw new Error(`The requested data was not found.`); 228 } 229 230 try { 231 return this.#previouslyRetrievedAdditionalInformationPromise; 232 } finally { 233 this.#previouslyRetrievedAdditionalInformationPromise = null; 234 } 235 } 236 237 isActive() { 238 if (!IS_SUPPORTED_PLATFORM) { 239 return false; 240 } 241 return Services.profiler.IsActive(); 242 } 243 244 isSupportedPlatform() { 245 return IS_SUPPORTED_PLATFORM; 246 } 247 248 /** 249 * Watch for events that happen within the browser. These can affect the 250 * current availability and state of the Gecko Profiler. 251 */ 252 _observe(subject, topic, _data) { 253 // Note! If emitting new events make sure and update the list of bridged 254 // events in the perf actor. 255 switch (topic) { 256 case "profiler-started": { 257 const param = subject.QueryInterface(Ci.nsIProfilerStartParams); 258 this.emit( 259 topic, 260 param.entries, 261 param.interval, 262 param.features, 263 param.duration, 264 param.activeTabID 265 ); 266 break; 267 } 268 case "profiler-stopped": 269 this.emit(topic); 270 break; 271 } 272 } 273 274 /** 275 * Lists the supported features of the profiler for the current browser. 276 * 277 * @returns {string[]} 278 */ 279 getSupportedFeatures() { 280 if (!IS_SUPPORTED_PLATFORM) { 281 return []; 282 } 283 return Services.profiler.GetFeatures(); 284 } 285 };