Bug 1839315: part 4) Link from `SheetLoadData::mWasAlternate` to spec. r=emilio DONTBUILD
[gecko.git] / tools / profiler / tests / shared-head.js
blobd1b2f6868a0dfa41efb86fbb7998d2525d165edb
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 /* globals Assert */
6 /* globals info */
8 /**
9  * This file contains utilities that can be shared between xpcshell tests and mochitests.
10  */
12 // The marker phases.
13 const INSTANT = 0;
14 const INTERVAL = 1;
15 const INTERVAL_START = 2;
16 const INTERVAL_END = 3;
18 // This Services declaration may shadow another from head.js, so define it as
19 // a var rather than a const.
21 const defaultSettings = {
22   entries: 8 * 1024 * 1024, // 8M entries = 64MB
23   interval: 1, // ms
24   features: [],
25   threads: ["GeckoMain"],
28 // Effectively `async`: Start the profiler and return the `startProfiler`
29 // promise that will get resolved when all child process have started their own
30 // profiler.
31 async function startProfiler(callersSettings) {
32   if (Services.profiler.IsActive()) {
33     Assert.ok(
34       Services.env.exists("MOZ_PROFILER_STARTUP"),
35       "The profiler is active at the begining of the test, " +
36         "the MOZ_PROFILER_STARTUP environment variable should be set."
37     );
38     if (Services.env.exists("MOZ_PROFILER_STARTUP")) {
39       // If the startup profiling environment variable exists, it is likely
40       // that tests are being profiled.
41       // Stop the profiler before starting profiler tests.
42       info(
43         "This test starts and stops the profiler and is not compatible " +
44           "with the use of MOZ_PROFILER_STARTUP. " +
45           "Stopping the profiler before starting the test."
46       );
47       await Services.profiler.StopProfiler();
48     } else {
49       throw new Error(
50         "The profiler must not be active before starting it in a test."
51       );
52     }
53   }
54   const settings = Object.assign({}, defaultSettings, callersSettings);
55   return Services.profiler.StartProfiler(
56     settings.entries,
57     settings.interval,
58     settings.features,
59     settings.threads,
60     0,
61     settings.duration
62   );
65 function startProfilerForMarkerTests() {
66   return startProfiler({
67     features: ["nostacksampling", "js"],
68     threads: ["GeckoMain", "DOM Worker"],
69   });
72 /**
73  * This is a helper function be able to run `await wait(500)`. Unfortunately
74  * this is needed as the act of collecting functions relies on the periodic
75  * sampling of the threads. See:
76  * https://bugzilla.mozilla.org/show_bug.cgi?id=1529053
77  *
78  * @param {number} time
79  * @returns {Promise}
80  */
81 function wait(time) {
82   return new Promise(resolve => {
83     // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
84     setTimeout(resolve, time);
85   });
88 /**
89  * Get the payloads of a type recursively, including from all subprocesses.
90  *
91  * @param {Object} profile The gecko profile.
92  * @param {string} type The marker payload type, e.g. "DiskIO".
93  * @param {Array} payloadTarget The recursive list of payloads.
94  * @return {Array} The final payloads.
95  */
96 function getPayloadsOfTypeFromAllThreads(profile, type, payloadTarget = []) {
97   for (const { markers } of profile.threads) {
98     for (const markerTuple of markers.data) {
99       const payload = markerTuple[markers.schema.data];
100       if (payload && payload.type === type) {
101         payloadTarget.push(payload);
102       }
103     }
104   }
106   for (const subProcess of profile.processes) {
107     getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
108   }
110   return payloadTarget;
114  * Get the payloads of a type from a single thread.
116  * @param {Object} thread The thread from a profile.
117  * @param {string} type The marker payload type, e.g. "DiskIO".
118  * @return {Array} The payloads.
119  */
120 function getPayloadsOfType(thread, type) {
121   const { markers } = thread;
122   const results = [];
123   for (const markerTuple of markers.data) {
124     const payload = markerTuple[markers.schema.data];
125     if (payload && payload.type === type) {
126       results.push(payload);
127     }
128   }
129   return results;
133  * Applies the marker schema to create individual objects for each marker
135  * @param {Object} thread The thread from a profile.
136  * @return {InflatedMarker[]} The markers.
137  */
138 function getInflatedMarkerData(thread) {
139   const { markers, stringTable } = thread;
140   return markers.data.map(markerTuple => {
141     const marker = {};
142     for (const [key, tupleIndex] of Object.entries(markers.schema)) {
143       marker[key] = markerTuple[tupleIndex];
144       if (key === "name") {
145         // Use the string from the string table.
146         marker[key] = stringTable[marker[key]];
147       }
148     }
149     return marker;
150   });
154  * Applies the marker schema to create individual objects for each marker, then
155  * keeps only the network markers that match the profiler tests.
157  * @param {Object} thread The thread from a profile.
158  * @return {InflatedMarker[]} The filtered network markers.
159  */
160 function getInflatedNetworkMarkers(thread) {
161   const markers = getInflatedMarkerData(thread);
162   return markers.filter(
163     m =>
164       m.data &&
165       m.data.type === "Network" &&
166       // We filter out network markers that aren't related to the test, to
167       // avoid intermittents.
168       m.data.URI.includes("/tools/profiler/")
169   );
173  * From a list of network markers, this returns pairs of start/stop markers.
174  * If a stop marker can't be found for a start marker, this will return an array
175  * of only 1 element.
177  * @param {InflatedMarker[]} networkMarkers Network markers
178  * @return {InflatedMarker[][]} Pairs of network markers
179  */
180 function getPairsOfNetworkMarkers(allNetworkMarkers) {
181   // For each 'start' marker we want to find the next 'stop' or 'redirect'
182   // marker with the same id.
183   const result = [];
184   const mapOfStartMarkers = new Map(); // marker id -> id in result array
185   for (const marker of allNetworkMarkers) {
186     const { data } = marker;
187     if (data.status === "STATUS_START") {
188       if (mapOfStartMarkers.has(data.id)) {
189         const previousMarker = result[mapOfStartMarkers.get(data.id)][0];
190         Assert.ok(
191           false,
192           `We found 2 start markers with the same id ${data.id}, without end marker in-between.` +
193             `The first marker has URI ${previousMarker.data.URI}, the second marker has URI ${data.URI}.` +
194             ` This should not happen.`
195         );
196         continue;
197       }
199       mapOfStartMarkers.set(data.id, result.length);
200       result.push([marker]);
201     } else {
202       // STOP or REDIRECT
203       if (!mapOfStartMarkers.has(data.id)) {
204         Assert.ok(
205           false,
206           `We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
207         );
208         continue;
209       }
210       result[mapOfStartMarkers.get(data.id)].push(marker);
211       mapOfStartMarkers.delete(data.id);
212     }
213   }
215   return result;
219  * It can be helpful to force the profiler to collect a JavaScript sample. This
220  * function spins on a while loop until at least one more sample is collected.
222  * @return {number} The index of the collected sample.
223  */
224 function captureAtLeastOneJsSample() {
225   function getProfileSampleCount() {
226     const profile = Services.profiler.getProfileData();
227     return profile.threads[0].samples.data.length;
228   }
230   const sampleCount = getProfileSampleCount();
231   // Create an infinite loop until a sample has been collected.
232   while (true) {
233     if (sampleCount < getProfileSampleCount()) {
234       return sampleCount;
235     }
236   }
239 function isJSONWhitespace(c) {
240   return ["\n", "\r", " ", "\t"].includes(c);
243 function verifyJSONStringIsCompact(s) {
244   const stateData = 0;
245   const stateString = 1;
246   const stateEscapedChar = 2;
247   let state = stateData;
248   for (let i = 0; i < s.length; ++i) {
249     let c = s[i];
250     switch (state) {
251       case stateData:
252         if (isJSONWhitespace(c)) {
253           Assert.ok(
254             false,
255             `"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
256           );
257           return;
258         }
259         if (c == '"') {
260           state = stateString;
261         }
262         break;
263       case stateString:
264         if (c == '"') {
265           state = stateData;
266         } else if (c == "\\") {
267           state = stateEscapedChar;
268         }
269         break;
270       case stateEscapedChar:
271         state = stateString;
272         break;
273     }
274   }
278  * This function pauses the profiler before getting the profile. Then after
279  * getting the data, the profiler is stopped, and all profiler data is removed.
280  * @returns {Promise<Profile>}
281  */
282 async function stopNowAndGetProfile() {
283   // Don't await the pause, because each process will handle it before it
284   // receives the following `getProfileDataAsArrayBuffer()`.
285   Services.profiler.Pause();
287   const profileArrayBuffer =
288     await Services.profiler.getProfileDataAsArrayBuffer();
289   await Services.profiler.StopProfiler();
291   const profileUint8Array = new Uint8Array(profileArrayBuffer);
292   const textDecoder = new TextDecoder("utf-8", { fatal: true });
293   const profileString = textDecoder.decode(profileUint8Array);
294   verifyJSONStringIsCompact(profileString);
296   return JSON.parse(profileString);
300  * This function ensures there's at least one sample, then pauses the profiler
301  * before getting the profile. Then after getting the data, the profiler is
302  * stopped, and all profiler data is removed.
303  * @returns {Promise<Profile>}
304  */
305 async function waitSamplingAndStopAndGetProfile() {
306   await Services.profiler.waitOnePeriodicSampling();
307   return stopNowAndGetProfile();
311  * Verifies that a marker is an interval marker.
313  * @param {InflatedMarker} marker
314  * @returns {boolean}
315  */
316 function isIntervalMarker(inflatedMarker) {
317   return (
318     inflatedMarker.phase === 1 &&
319     typeof inflatedMarker.startTime === "number" &&
320     typeof inflatedMarker.endTime === "number"
321   );
325  * @param {Profile} profile
326  * @returns {Thread[]}
327  */
328 function getThreads(profile) {
329   const threads = [];
331   function getThreadsRecursive(process) {
332     for (const thread of process.threads) {
333       threads.push(thread);
334     }
335     for (const subprocess of process.processes) {
336       getThreadsRecursive(subprocess);
337     }
338   }
340   getThreadsRecursive(profile);
341   return threads;
345  * Find a specific marker schema from any process of a profile.
347  * @param {Profile} profile
348  * @param {string} name
349  * @returns {MarkerSchema}
350  */
351 function getSchema(profile, name) {
352   {
353     const schema = profile.meta.markerSchema.find(s => s.name === name);
354     if (schema) {
355       return schema;
356     }
357   }
358   for (const subprocess of profile.processes) {
359     const schema = subprocess.meta.markerSchema.find(s => s.name === name);
360     if (schema) {
361       return schema;
362     }
363   }
364   console.error("Parent process schema", profile.meta.markerSchema);
365   for (const subprocess of profile.processes) {
366     console.error("Child process schema", subprocess.meta.markerSchema);
367   }
368   throw new Error(`Could not find a schema for "${name}".`);
372  * This escapes all characters that have a special meaning in RegExps.
373  * This was stolen from https://github.com/sindresorhus/escape-string-regexp and
374  * so it is licence MIT and:
375  * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com).
376  * See the full license in https://raw.githubusercontent.com/sindresorhus/escape-string-regexp/main/license.
377  * @param {string} string The string to be escaped
378  * @returns {string} The result
379  */
380 function escapeStringRegexp(string) {
381   if (typeof string !== "string") {
382     throw new TypeError("Expected a string");
383   }
385   // Escape characters with special meaning either inside or outside character
386   // sets.  Use a simple backslash escape when it’s always valid, and a `\xnn`
387   // escape when the simpler form would be disallowed by Unicode patterns’
388   // stricter grammar.
389   return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
392 /** ------ Assertions helper ------ */
394  * This assert helper function makes it easy to check a lot of properties in an
395  * object. We augment Assert.sys.mjs to make it easier to use.
396  */
397 Object.assign(Assert, {
398   /*
399    * It checks if the properties on the right are all present in the object on
400    * the left. Note that the object might still have other properties (see
401    * objectContainsOnly below if you want the stricter form).
402    *
403    * The basic form does basic equality on each expected property:
404    *
405    * Assert.objectContains(fixture, {
406    *   foo: "foo",
407    *   bar: 1,
408    *   baz: true,
409    * });
410    *
411    * But it also has a more powerful form with expectations. The available
412    * expectations are:
413    * - any(): this only checks for the existence of the property, not its value
414    * - number(), string(), boolean(), bigint(), function(), symbol(), object():
415    *   this checks if the value is of this type
416    * - objectContains(expected): this applies Assert.objectContains()
417    *   recursively on this property.
418    * - stringContains(needle): this checks if the expected value is included in
419    *   the property value.
420    * - stringMatches(regexp): this checks if the property value matches this
421    *   regexp. The regexp can be passed as a string, to be dynamically built.
422    *
423    * example:
424    *
425    * Assert.objectContains(fixture, {
426    *   name: Expect.stringMatches(`Load \\d+:.*${url}`),
427    *   data: Expect.objectContains({
428    *     status: "STATUS_STOP",
429    *     URI: Expect.stringContains("https://"),
430    *     requestMethod: "GET",
431    *     contentType: Expect.string(),
432    *     startTime: Expect.number(),
433    *     cached: Expect.boolean(),
434    *   }),
435    * });
436    *
437    * Each expectation will translate into one or more Assert call. Therefore if
438    * one expectation fails, this will be clearly visible in the test output.
439    *
440    * Expectations can also be normal functions, for example:
441    *
442    * Assert.objectContains(fixture, {
443    *   number: value => Assert.greater(value, 5)
444    * });
445    *
446    * Note that you'll need to use Assert inside this function.
447    */
448   objectContains(object, expectedProperties) {
449     // Basic tests: we don't want to run other assertions if these tests fail.
450     if (typeof object !== "object") {
451       this.ok(
452         false,
453         `The first parameter should be an object, but found: ${object}.`
454       );
455       return;
456     }
458     if (typeof expectedProperties !== "object") {
459       this.ok(
460         false,
461         `The second parameter should be an object, but found: ${expectedProperties}.`
462       );
463       return;
464     }
466     for (const key of Object.keys(expectedProperties)) {
467       const expected = expectedProperties[key];
468       if (!(key in object)) {
469         this.report(
470           true,
471           object,
472           expectedProperties,
473           `The object should contain the property "${key}", but it's missing.`
474         );
475         continue;
476       }
478       if (typeof expected === "function") {
479         // This is a function, so let's call it.
480         expected(
481           object[key],
482           `The object should contain the property "${key}" with an expected value and type.`
483         );
484       } else {
485         // Otherwise, we check for equality.
486         this.equal(
487           object[key],
488           expectedProperties[key],
489           `The object should contain the property "${key}" with an expected value.`
490         );
491       }
492     }
493   },
495   /**
496    * This is very similar to the previous `objectContains`, but this also looks
497    * at the number of the objects' properties. Thus this will fail if the
498    * objects don't have the same properties exactly.
499    */
500   objectContainsOnly(object, expectedProperties) {
501     // Basic tests: we don't want to run other assertions if these tests fail.
502     if (typeof object !== "object") {
503       this.ok(
504         false,
505         `The first parameter should be an object but found: ${object}.`
506       );
507       return;
508     }
510     if (typeof expectedProperties !== "object") {
511       this.ok(
512         false,
513         `The second parameter should be an object but found: ${expectedProperties}.`
514       );
515       return;
516     }
518     // In objectContainsOnly, we specifically want to check if all properties
519     // from the fixture object are expected.
520     // We'll be failing a test only for the specific properties that weren't
521     // expected, and only fail with one message, so that the test outputs aren't
522     // spammed.
523     const extraProperties = [];
524     for (const fixtureKey of Object.keys(object)) {
525       if (!(fixtureKey in expectedProperties)) {
526         extraProperties.push(fixtureKey);
527       }
528     }
530     if (extraProperties.length) {
531       // Some extra properties have been found.
532       this.report(
533         true,
534         object,
535         expectedProperties,
536         `These properties are present, but shouldn't: "${extraProperties.join(
537           '", "'
538         )}".`
539       );
540     }
542     // Now, let's carry on the rest of our work.
543     this.objectContains(object, expectedProperties);
544   },
547 const Expect = {
548   any:
549     () =>
550     actual => {} /* We don't check anything more than the presence of this property. */,
553 /* These functions are part of the Assert object, and we want to reuse them. */
555   "stringContains",
556   "stringMatches",
557   "objectContains",
558   "objectContainsOnly",
559 ].forEach(
560   assertChecker =>
561     (Expect[assertChecker] =
562       expected =>
563       (actual, ...moreArgs) =>
564         Assert[assertChecker](actual, expected, ...moreArgs))
567 /* These functions will only check for the type. */
569   "number",
570   "string",
571   "boolean",
572   "bigint",
573   "symbol",
574   "object",
575   "function",
576 ].forEach(type => (Expect[type] = makeTypeChecker(type)));
578 function makeTypeChecker(type) {
579   return (...unexpectedArgs) => {
580     if (unexpectedArgs.length) {
581       throw new Error(
582         "Type checkers expectations aren't expecting any argument."
583       );
584     }
585     return (actual, message) => {
586       const isCorrect = typeof actual === type;
587       Assert.report(!isCorrect, actual, type, message, "has type");
588     };
589   };
591 /* ------ End of assertion helper ------ */