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/. */
5 import { Assert } from "resource://testing-common/Assert.sys.mjs";
6 import { StructuredLogger } from "resource://testing-common/StructuredLog.sys.mjs";
9 * This module implements useful utilites for interacting with the profiler,
10 * as well as querying profiles captured during tests.
12 export var ProfilerTestUtils = {
22 * This is a helper function to start the profiler with a settings object,
23 * while additionally performing checks to ensure that the profiler is not
24 * already running when we call this function.
26 * @param {Object} callersSettings The settings object to deconstruct and pass
27 * to the profiler. Unspecified settings are overwritten by the default:
29 * entries: 8 * 1024 * 1024
32 * threads: ["GeckoMain"]
36 * @returns {Promise} The promise returned by StartProfiler. This will resolve
37 * once all child processes have started their own profiler.
39 async startProfiler(callersSettings) {
40 const defaultSettings = {
41 entries: 8 * 1024 * 1024, // 8M entries = 64MB
44 threads: ["GeckoMain"],
48 if (Services.profiler.IsActive()) {
50 Services.env.exists("MOZ_PROFILER_STARTUP"),
51 "The profiler is active at the begining of the test, " +
52 "the MOZ_PROFILER_STARTUP environment variable should be set."
54 if (Services.env.exists("MOZ_PROFILER_STARTUP")) {
55 // If the startup profiling environment variable exists, it is likely
56 // that tests are being profiled.
57 // Stop the profiler before starting profiler tests.
58 StructuredLogger.info(
59 "This test starts and stops the profiler and is not compatible " +
60 "with the use of MOZ_PROFILER_STARTUP. " +
61 "Stopping the profiler before starting the test."
63 await Services.profiler.StopProfiler();
66 "The profiler must not be active before starting it in a test."
70 const settings = Object.assign({}, defaultSettings, callersSettings);
71 return Services.profiler.StartProfiler(
82 * This is a helper function to start the profiler for marker tests, and is
83 * just a wrapper around `startProfiler` with some specific defaults.
85 async startProfilerForMarkerTests() {
86 return this.startProfiler({
87 features: ["nostacksampling", "js"],
88 threads: ["GeckoMain", "DOM Worker"],
93 * Get the payloads of a type recursively, including from all subprocesses.
95 * @param {Object} profile The gecko profile.
96 * @param {string} type The marker payload type, e.g. "DiskIO".
97 * @param {Array} payloadTarget The recursive list of payloads.
98 * @return {Array} The final payloads.
100 getPayloadsOfTypeFromAllThreads(profile, type, payloadTarget = []) {
101 for (const { markers } of profile.threads) {
102 for (const markerTuple of markers.data) {
103 const payload = markerTuple[markers.schema.data];
104 if (payload && payload.type === type) {
105 payloadTarget.push(payload);
110 for (const subProcess of profile.processes) {
111 this.getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
114 return payloadTarget;
118 * Get the payloads of a type from a single thread.
120 * @param {Object} thread The thread from a profile.
121 * @param {string} type The marker payload type, e.g. "DiskIO".
122 * @return {Array} The payloads.
124 getPayloadsOfType(thread, type) {
125 const { markers } = thread;
127 for (const markerTuple of markers.data) {
128 const payload = markerTuple[markers.schema.data];
129 if (payload && payload.type === type) {
130 results.push(payload);
137 * Applies the marker schema to create individual objects for each marker
139 * @param {Object} thread The thread from a profile.
140 * @return {InflatedMarker[]} The markers.
142 getInflatedMarkerData(thread) {
143 const { markers, stringTable } = thread;
144 return markers.data.map(markerTuple => {
146 for (const [key, tupleIndex] of Object.entries(markers.schema)) {
147 marker[key] = markerTuple[tupleIndex];
148 if (key === "name") {
149 // Use the string from the string table.
150 marker[key] = stringTable[marker[key]];
158 * Applies the marker schema to create individual objects for each marker, then
159 * keeps only the network markers that match the profiler tests.
161 * @param {Object} thread The thread from a profile.
162 * @return {InflatedMarker[]} The filtered network markers.
164 getInflatedNetworkMarkers(thread) {
165 const markers = this.getInflatedMarkerData(thread);
166 return markers.filter(
169 m.data.type === "Network" &&
170 // We filter out network markers that aren't related to the test, to
171 // avoid intermittents.
172 m.data.URI.includes("/tools/profiler/")
177 * From a list of network markers, this returns pairs of start/stop markers.
178 * If a stop marker can't be found for a start marker, this will return an array
181 * @param {InflatedMarker[]} networkMarkers Network markers
182 * @return {InflatedMarker[][]} Pairs of network markers
184 getPairsOfNetworkMarkers(allNetworkMarkers) {
185 // For each 'start' marker we want to find the next 'stop' or 'redirect'
186 // marker with the same id.
188 const mapOfStartMarkers = new Map(); // marker id -> id in result array
189 for (const marker of allNetworkMarkers) {
190 const { data } = marker;
191 if (data.status === "STATUS_START") {
192 if (mapOfStartMarkers.has(data.id)) {
193 const previousMarker = result[mapOfStartMarkers.get(data.id)][0];
196 `We found 2 start markers with the same id ${data.id}, without end marker in-between.` +
197 `The first marker has URI ${previousMarker.data.URI}, the second marker has URI ${data.URI}.` +
198 ` This should not happen.`
203 mapOfStartMarkers.set(data.id, result.length);
204 result.push([marker]);
207 if (!mapOfStartMarkers.has(data.id)) {
210 `We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
214 result[mapOfStartMarkers.get(data.id)].push(marker);
215 mapOfStartMarkers.delete(data.id);
223 * It can be helpful to force the profiler to collect a JavaScript sample. This
224 * function spins on a while loop until at least one more sample is collected.
226 * @return {number} The index of the collected sample.
228 captureAtLeastOneJsSample() {
229 function getProfileSampleCount() {
230 const profile = Services.profiler.getProfileData();
231 return profile.threads[0].samples.data.length;
234 const sampleCount = getProfileSampleCount();
235 // Create an infinite loop until a sample has been collected.
236 // eslint-disable-next-line no-constant-condition
238 if (sampleCount < getProfileSampleCount()) {
245 * Verify that a given JSON string is compact - i.e. does not contain
246 * unexpected whitespace.
248 * @param {String} the JSON string to check
249 * @return {Bool} Whether the string is compact or not
251 verifyJSONStringIsCompact(s) {
252 function isJSONWhitespace(c) {
253 return ["\n", "\r", " ", "\t"].includes(c);
256 const stateString = 1;
257 const stateEscapedChar = 2;
258 let state = stateData;
259 for (let i = 0; i < s.length; ++i) {
263 if (isJSONWhitespace(c)) {
266 `"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
277 } else if (c == "\\") {
278 state = stateEscapedChar;
281 case stateEscapedChar:
289 * This function pauses the profiler before getting the profile. Then after
290 * getting the data, the profiler is stopped, and all profiler data is removed.
291 * @returns {Promise<Profile>}
293 async stopNowAndGetProfile() {
294 // Don't await the pause, because each process will handle it before it
295 // receives the following `getProfileDataAsArrayBuffer()`.
296 Services.profiler.Pause();
298 const profileArrayBuffer =
299 await Services.profiler.getProfileDataAsArrayBuffer();
300 await Services.profiler.StopProfiler();
302 const profileUint8Array = new Uint8Array(profileArrayBuffer);
303 const textDecoder = new TextDecoder("utf-8", { fatal: true });
304 const profileString = textDecoder.decode(profileUint8Array);
305 this.verifyJSONStringIsCompact(profileString);
307 return JSON.parse(profileString);
311 * This function ensures there's at least one sample, then pauses the profiler
312 * before getting the profile. Then after getting the data, the profiler is
313 * stopped, and all profiler data is removed.
314 * @returns {Promise<Profile>}
316 async waitSamplingAndStopAndGetProfile() {
317 await Services.profiler.waitOnePeriodicSampling();
318 return this.stopNowAndGetProfile();
322 * Verifies that a marker is an interval marker.
324 * @param {InflatedMarker} marker
327 isIntervalMarker(inflatedMarker) {
329 inflatedMarker.phase === 1 &&
330 typeof inflatedMarker.startTime === "number" &&
331 typeof inflatedMarker.endTime === "number"
336 * @param {Profile} profile
337 * @returns {Thread[]}
339 getThreads(profile) {
342 function getThreadsRecursive(process) {
343 for (const thread of process.threads) {
344 threads.push(thread);
346 for (const subprocess of process.processes) {
347 getThreadsRecursive(subprocess);
351 getThreadsRecursive(profile);
356 * Find a specific marker schema from any process of a profile.
358 * @param {Profile} profile
359 * @param {string} name
360 * @returns {MarkerSchema}
362 getSchema(profile, name) {
364 const schema = profile.meta.markerSchema.find(s => s.name === name);
369 for (const subprocess of profile.processes) {
370 const schema = subprocess.meta.markerSchema.find(s => s.name === name);
375 console.error("Parent process schema", profile.meta.markerSchema);
376 for (const subprocess of profile.processes) {
377 console.error("Child process schema", subprocess.meta.markerSchema);
379 throw new Error(`Could not find a schema for "${name}".`);