Backed out 2 changesets (bug 1943998) for causing wd failures @ phases.py CLOSED...
[gecko.git] / tools / profiler / tests / ProfilerTestUtils.sys.mjs
blobbf5767c293a0435dd56195ae94c4e24d16fd6723
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";
8 /*
9  * This module implements useful utilites for interacting with the profiler,
10  * as well as querying profiles captured during tests.
11  */
12 export var ProfilerTestUtils = {
13   // The marker phases.
14   markerPhases: {
15     INSTANT: 0,
16     INTERVAL: 1,
17     INTERVAL_START: 2,
18     INTERVAL_END: 3,
19   },
21   /**
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.
25    *
26    * @param {Object} callersSettings The settings object to deconstruct and pass
27    *   to the profiler. Unspecified settings are overwritten by the default:
28    *   {
29    *     entries: 8 * 1024 * 1024
30    *     interval: 1
31    *     features: []
32    *     threads: ["GeckoMain"]
33    *     activeTabId: 0
34    *     duration: 0
35    *   }
36    * @returns {Promise} The promise returned by StartProfiler. This will resolve
37    *   once all child processes have started their own profiler.
38    */
39   async startProfiler(callersSettings) {
40     const defaultSettings = {
41       entries: 8 * 1024 * 1024, // 8M entries = 64MB
42       interval: 1, // ms
43       features: [],
44       threads: ["GeckoMain"],
45       activeTabId: 0,
46       duration: 0,
47     };
48     if (Services.profiler.IsActive()) {
49       Assert.ok(
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."
53       );
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."
62         );
63         await Services.profiler.StopProfiler();
64       } else {
65         throw new Error(
66           "The profiler must not be active before starting it in a test."
67         );
68       }
69     }
70     const settings = Object.assign({}, defaultSettings, callersSettings);
71     return Services.profiler.StartProfiler(
72       settings.entries,
73       settings.interval,
74       settings.features,
75       settings.threads,
76       settings.activeTabId,
77       settings.duration
78     );
79   },
81   /**
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.
84    */
85   async startProfilerForMarkerTests() {
86     return this.startProfiler({
87       features: ["nostacksampling", "js"],
88       threads: ["GeckoMain", "DOM Worker"],
89     });
90   },
92   /**
93    * Get the payloads of a type recursively, including from all subprocesses.
94    *
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.
99    */
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);
106         }
107       }
108     }
110     for (const subProcess of profile.processes) {
111       this.getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
112     }
114     return payloadTarget;
115   },
117   /**
118    * Get the payloads of a type from a single thread.
119    *
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.
123    */
124   getPayloadsOfType(thread, type) {
125     const { markers } = thread;
126     const results = [];
127     for (const markerTuple of markers.data) {
128       const payload = markerTuple[markers.schema.data];
129       if (payload && payload.type === type) {
130         results.push(payload);
131       }
132     }
133     return results;
134   },
136   /**
137    * Applies the marker schema to create individual objects for each marker
138    *
139    * @param {Object} thread The thread from a profile.
140    * @return {InflatedMarker[]} The markers.
141    */
142   getInflatedMarkerData(thread) {
143     const { markers, stringTable } = thread;
144     return markers.data.map(markerTuple => {
145       const marker = {};
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]];
151         }
152       }
153       return marker;
154     });
155   },
157   /**
158    * Applies the marker schema to create individual objects for each marker, then
159    * keeps only the network markers that match the profiler tests.
160    *
161    * @param {Object} thread The thread from a profile.
162    * @return {InflatedMarker[]} The filtered network markers.
163    */
164   getInflatedNetworkMarkers(thread) {
165     const markers = this.getInflatedMarkerData(thread);
166     return markers.filter(
167       m =>
168         m.data &&
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/")
173     );
174   },
176   /**
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
179    * of only 1 element.
180    *
181    * @param {InflatedMarker[]} networkMarkers Network markers
182    * @return {InflatedMarker[][]} Pairs of network markers
183    */
184   getPairsOfNetworkMarkers(allNetworkMarkers) {
185     // For each 'start' marker we want to find the next 'stop' or 'redirect'
186     // marker with the same id.
187     const result = [];
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];
194           Assert.ok(
195             false,
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.`
199           );
200           continue;
201         }
203         mapOfStartMarkers.set(data.id, result.length);
204         result.push([marker]);
205       } else {
206         // STOP or REDIRECT
207         if (!mapOfStartMarkers.has(data.id)) {
208           Assert.ok(
209             false,
210             `We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
211           );
212           continue;
213         }
214         result[mapOfStartMarkers.get(data.id)].push(marker);
215         mapOfStartMarkers.delete(data.id);
216       }
217     }
219     return result;
220   },
222   /**
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.
225    *
226    * @return {number} The index of the collected sample.
227    */
228   captureAtLeastOneJsSample() {
229     function getProfileSampleCount() {
230       const profile = Services.profiler.getProfileData();
231       return profile.threads[0].samples.data.length;
232     }
234     const sampleCount = getProfileSampleCount();
235     // Create an infinite loop until a sample has been collected.
236     // eslint-disable-next-line no-constant-condition
237     while (true) {
238       if (sampleCount < getProfileSampleCount()) {
239         return sampleCount;
240       }
241     }
242   },
244   /**
245    * Verify that a given JSON string is compact - i.e. does not contain
246    * unexpected whitespace.
247    *
248    * @param {String} the JSON string to check
249    * @return {Bool} Whether the string is compact or not
250    */
251   verifyJSONStringIsCompact(s) {
252     function isJSONWhitespace(c) {
253       return ["\n", "\r", " ", "\t"].includes(c);
254     }
255     const stateData = 0;
256     const stateString = 1;
257     const stateEscapedChar = 2;
258     let state = stateData;
259     for (let i = 0; i < s.length; ++i) {
260       let c = s[i];
261       switch (state) {
262         case stateData:
263           if (isJSONWhitespace(c)) {
264             Assert.ok(
265               false,
266               `"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
267             );
268             return;
269           }
270           if (c == '"') {
271             state = stateString;
272           }
273           break;
274         case stateString:
275           if (c == '"') {
276             state = stateData;
277           } else if (c == "\\") {
278             state = stateEscapedChar;
279           }
280           break;
281         case stateEscapedChar:
282           state = stateString;
283           break;
284       }
285     }
286   },
288   /**
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>}
292    */
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);
308   },
310   /**
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>}
315    */
316   async waitSamplingAndStopAndGetProfile() {
317     await Services.profiler.waitOnePeriodicSampling();
318     return this.stopNowAndGetProfile();
319   },
321   /**
322    * Verifies that a marker is an interval marker.
323    *
324    * @param {InflatedMarker} marker
325    * @returns {boolean}
326    */
327   isIntervalMarker(inflatedMarker) {
328     return (
329       inflatedMarker.phase === 1 &&
330       typeof inflatedMarker.startTime === "number" &&
331       typeof inflatedMarker.endTime === "number"
332     );
333   },
335   /**
336    * @param {Profile} profile
337    * @returns {Thread[]}
338    */
339   getThreads(profile) {
340     const threads = [];
342     function getThreadsRecursive(process) {
343       for (const thread of process.threads) {
344         threads.push(thread);
345       }
346       for (const subprocess of process.processes) {
347         getThreadsRecursive(subprocess);
348       }
349     }
351     getThreadsRecursive(profile);
352     return threads;
353   },
355   /**
356    * Find a specific marker schema from any process of a profile.
357    *
358    * @param {Profile} profile
359    * @param {string} name
360    * @returns {MarkerSchema}
361    */
362   getSchema(profile, name) {
363     {
364       const schema = profile.meta.markerSchema.find(s => s.name === name);
365       if (schema) {
366         return schema;
367       }
368     }
369     for (const subprocess of profile.processes) {
370       const schema = subprocess.meta.markerSchema.find(s => s.name === name);
371       if (schema) {
372         return schema;
373       }
374     }
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);
378     }
379     throw new Error(`Could not find a schema for "${name}".`);
380   },