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/. */
9 * This file contains utilities that can be shared between xpcshell tests and mochitests.
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
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
31 async function startProfiler(callersSettings) {
32 if (Services.profiler.IsActive()) {
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."
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.
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."
47 await Services.profiler.StopProfiler();
50 "The profiler must not be active before starting it in a test."
54 const settings = Object.assign({}, defaultSettings, callersSettings);
55 return Services.profiler.StartProfiler(
65 function startProfilerForMarkerTests() {
66 return startProfiler({
67 features: ["nostacksampling", "js"],
68 threads: ["GeckoMain", "DOM Worker"],
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
78 * @param {number} time
82 return new Promise(resolve => {
83 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
84 setTimeout(resolve, time);
89 * Get the payloads of a type recursively, including from all subprocesses.
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.
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);
106 for (const subProcess of profile.processes) {
107 getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
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.
120 function getPayloadsOfType(thread, type) {
121 const { markers } = thread;
123 for (const markerTuple of markers.data) {
124 const payload = markerTuple[markers.schema.data];
125 if (payload && payload.type === type) {
126 results.push(payload);
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.
138 function getInflatedMarkerData(thread) {
139 const { markers, stringTable } = thread;
140 return markers.data.map(markerTuple => {
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]];
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.
160 function getInflatedNetworkMarkers(thread) {
161 const markers = getInflatedMarkerData(thread);
162 return markers.filter(
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/")
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
177 * @param {InflatedMarker[]} networkMarkers Network markers
178 * @return {InflatedMarker[][]} Pairs of network markers
180 function getPairsOfNetworkMarkers(allNetworkMarkers) {
181 // For each 'start' marker we want to find the next 'stop' or 'redirect'
182 // marker with the same id.
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];
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.`
199 mapOfStartMarkers.set(data.id, result.length);
200 result.push([marker]);
203 if (!mapOfStartMarkers.has(data.id)) {
206 `We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
210 result[mapOfStartMarkers.get(data.id)].push(marker);
211 mapOfStartMarkers.delete(data.id);
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.
224 function captureAtLeastOneJsSample() {
225 function getProfileSampleCount() {
226 const profile = Services.profiler.getProfileData();
227 return profile.threads[0].samples.data.length;
230 const sampleCount = getProfileSampleCount();
231 // Create an infinite loop until a sample has been collected.
233 if (sampleCount < getProfileSampleCount()) {
239 function isJSONWhitespace(c) {
240 return ["\n", "\r", " ", "\t"].includes(c);
243 function verifyJSONStringIsCompact(s) {
245 const stateString = 1;
246 const stateEscapedChar = 2;
247 let state = stateData;
248 for (let i = 0; i < s.length; ++i) {
252 if (isJSONWhitespace(c)) {
255 `"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
266 } else if (c == "\\") {
267 state = stateEscapedChar;
270 case stateEscapedChar:
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>}
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>}
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
316 function isIntervalMarker(inflatedMarker) {
318 inflatedMarker.phase === 1 &&
319 typeof inflatedMarker.startTime === "number" &&
320 typeof inflatedMarker.endTime === "number"
325 * @param {Profile} profile
326 * @returns {Thread[]}
328 function getThreads(profile) {
331 function getThreadsRecursive(process) {
332 for (const thread of process.threads) {
333 threads.push(thread);
335 for (const subprocess of process.processes) {
336 getThreadsRecursive(subprocess);
340 getThreadsRecursive(profile);
345 * Find a specific marker schema from any process of a profile.
347 * @param {Profile} profile
348 * @param {string} name
349 * @returns {MarkerSchema}
351 function getSchema(profile, name) {
353 const schema = profile.meta.markerSchema.find(s => s.name === name);
358 for (const subprocess of profile.processes) {
359 const schema = subprocess.meta.markerSchema.find(s => s.name === name);
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);
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
380 function escapeStringRegexp(string) {
381 if (typeof string !== "string") {
382 throw new TypeError("Expected a string");
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’
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.
397 Object.assign(Assert, {
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).
403 * The basic form does basic equality on each expected property:
405 * Assert.objectContains(fixture, {
411 * But it also has a more powerful form with expectations. The available
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.
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(),
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.
440 * Expectations can also be normal functions, for example:
442 * Assert.objectContains(fixture, {
443 * number: value => Assert.greater(value, 5)
446 * Note that you'll need to use Assert inside this function.
448 objectContains(object, expectedProperties) {
449 // Basic tests: we don't want to run other assertions if these tests fail.
450 if (typeof object !== "object") {
453 `The first parameter should be an object, but found: ${object}.`
458 if (typeof expectedProperties !== "object") {
461 `The second parameter should be an object, but found: ${expectedProperties}.`
466 for (const key of Object.keys(expectedProperties)) {
467 const expected = expectedProperties[key];
468 if (!(key in object)) {
473 `The object should contain the property "${key}", but it's missing.`
478 if (typeof expected === "function") {
479 // This is a function, so let's call it.
482 `The object should contain the property "${key}" with an expected value and type.`
485 // Otherwise, we check for equality.
488 expectedProperties[key],
489 `The object should contain the property "${key}" with an expected value.`
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.
500 objectContainsOnly(object, expectedProperties) {
501 // Basic tests: we don't want to run other assertions if these tests fail.
502 if (typeof object !== "object") {
505 `The first parameter should be an object but found: ${object}.`
510 if (typeof expectedProperties !== "object") {
513 `The second parameter should be an object but found: ${expectedProperties}.`
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
523 const extraProperties = [];
524 for (const fixtureKey of Object.keys(object)) {
525 if (!(fixtureKey in expectedProperties)) {
526 extraProperties.push(fixtureKey);
530 if (extraProperties.length) {
531 // Some extra properties have been found.
536 `These properties are present, but shouldn't: "${extraProperties.join(
542 // Now, let's carry on the rest of our work.
543 this.objectContains(object, expectedProperties);
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. */
558 "objectContainsOnly",
561 (Expect[assertChecker] =
563 (actual, ...moreArgs) =>
564 Assert[assertChecker](actual, expected, ...moreArgs))
567 /* These functions will only check for the type. */
576 ].forEach(type => (Expect[type] = makeTypeChecker(type)));
578 function makeTypeChecker(type) {
579 return (...unexpectedArgs) => {
580 if (unexpectedArgs.length) {
582 "Type checkers expectations aren't expecting any argument."
585 return (actual, message) => {
586 const isCorrect = typeof actual === type;
587 Assert.report(!isCorrect, actual, type, message, "has type");
591 /* ------ End of assertion helper ------ */