Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/*
* This module implements useful utilites for interacting with the profiler,
* as well as querying profiles captured during tests.
*/
export var ProfilerTestUtils = {
// The marker phases.
markerPhases: {
INSTANT: 0,
INTERVAL: 1,
INTERVAL_START: 2,
INTERVAL_END: 3,
},
/**
* This is a helper function to start the profiler with a settings object,
* while additionally performing checks to ensure that the profiler is not
* already running when we call this function.
*
* @param {Object} callersSettings The settings object to deconstruct and pass
* to the profiler. Unspecified settings are overwritten by the default:
* {
* entries: 8 * 1024 * 1024
* interval: 1
* features: []
* threads: ["GeckoMain"]
* activeTabId: 0
* duration: 0
* }
* @returns {Promise} The promise returned by StartProfiler. This will resolve
* once all child processes have started their own profiler.
*/
async startProfiler(callersSettings) {
const defaultSettings = {
entries: 8 * 1024 * 1024, // 8M entries = 64MB
interval: 1, // ms
features: [],
threads: ["GeckoMain"],
activeTabId: 0,
duration: 0,
};
if (Services.profiler.IsActive()) {
Assert.ok(
Services.env.exists("MOZ_PROFILER_STARTUP"),
"The profiler is active at the begining of the test, " +
"the MOZ_PROFILER_STARTUP environment variable should be set."
);
if (Services.env.exists("MOZ_PROFILER_STARTUP")) {
// If the startup profiling environment variable exists, it is likely
// that tests are being profiled.
// Stop the profiler before starting profiler tests.
StructuredLogger.info(
"This test starts and stops the profiler and is not compatible " +
"with the use of MOZ_PROFILER_STARTUP. " +
"Stopping the profiler before starting the test."
);
await Services.profiler.StopProfiler();
} else {
throw new Error(
"The profiler must not be active before starting it in a test."
);
}
}
const settings = Object.assign({}, defaultSettings, callersSettings);
return Services.profiler.StartProfiler(
settings.entries,
settings.interval,
settings.features,
settings.threads,
settings.activeTabId,
settings.duration
);
},
/**
* This is a helper function to start the profiler for marker tests, and is
* just a wrapper around `startProfiler` with some specific defaults.
*/
async startProfilerForMarkerTests() {
return this.startProfiler({
features: ["nostacksampling", "js"],
threads: ["GeckoMain", "DOM Worker"],
});
},
/**
* Get the payloads of a type recursively, including from all subprocesses.
*
* @param {Object} profile The gecko profile.
* @param {string} type The marker payload type, e.g. "DiskIO".
* @param {Array} payloadTarget The recursive list of payloads.
* @return {Array} The final payloads.
*/
getPayloadsOfTypeFromAllThreads(profile, type, payloadTarget = []) {
for (const { markers } of profile.threads) {
for (const markerTuple of markers.data) {
const payload = markerTuple[markers.schema.data];
if (payload && payload.type === type) {
payloadTarget.push(payload);
}
}
}
for (const subProcess of profile.processes) {
this.getPayloadsOfTypeFromAllThreads(subProcess, type, payloadTarget);
}
return payloadTarget;
},
/**
* Get the payloads of a type from a single thread.
*
* @param {Object} thread The thread from a profile.
* @param {string} type The marker payload type, e.g. "DiskIO".
* @return {Array} The payloads.
*/
getPayloadsOfType(thread, type) {
const { markers } = thread;
const results = [];
for (const markerTuple of markers.data) {
const payload = markerTuple[markers.schema.data];
if (payload && payload.type === type) {
results.push(payload);
}
}
return results;
},
/**
* Applies the marker schema to create individual objects for each marker
*
* @param {Object} thread The thread from a profile.
* @return {InflatedMarker[]} The markers.
*/
getInflatedMarkerData(thread) {
const { markers, stringTable } = thread;
return markers.data.map(markerTuple => {
const marker = {};
for (const [key, tupleIndex] of Object.entries(markers.schema)) {
marker[key] = markerTuple[tupleIndex];
if (key === "name") {
// Use the string from the string table.
marker[key] = stringTable[marker[key]];
}
}
return marker;
});
},
/**
* Applies the marker schema to create individual objects for each marker, then
* keeps only the network markers that match the profiler tests.
*
* @param {Object} thread The thread from a profile.
* @return {InflatedMarker[]} The filtered network markers.
*/
getInflatedNetworkMarkers(thread) {
const markers = this.getInflatedMarkerData(thread);
return markers.filter(
m =>
m.data &&
m.data.type === "Network" &&
// We filter out network markers that aren't related to the test, to
// avoid intermittents.
m.data.URI.includes("/tools/profiler/")
);
},
/**
* From a list of network markers, this returns pairs of start/stop markers.
* If a stop marker can't be found for a start marker, this will return an array
* of only 1 element.
*
* @param {InflatedMarker[]} networkMarkers Network markers
* @return {InflatedMarker[][]} Pairs of network markers
*/
getPairsOfNetworkMarkers(allNetworkMarkers) {
// For each 'start' marker we want to find the next 'stop' or 'redirect'
// marker with the same id.
const result = [];
const mapOfStartMarkers = new Map(); // marker id -> id in result array
for (const marker of allNetworkMarkers) {
const { data } = marker;
if (data.status === "STATUS_START") {
if (mapOfStartMarkers.has(data.id)) {
const previousMarker = result[mapOfStartMarkers.get(data.id)][0];
Assert.ok(
false,
`We found 2 start markers with the same id ${data.id}, without end marker in-between.` +
`The first marker has URI ${previousMarker.data.URI}, the second marker has URI ${data.URI}.` +
` This should not happen.`
);
continue;
}
mapOfStartMarkers.set(data.id, result.length);
result.push([marker]);
} else {
// STOP or REDIRECT
if (!mapOfStartMarkers.has(data.id)) {
Assert.ok(
false,
`We found an end marker without a start marker (id: ${data.id}, URI: ${data.URI}). This should not happen.`
);
continue;
}
result[mapOfStartMarkers.get(data.id)].push(marker);
mapOfStartMarkers.delete(data.id);
}
}
return result;
},
/**
* It can be helpful to force the profiler to collect a JavaScript sample. This
* function spins on a while loop until at least one more sample is collected.
*
* @return {number} The index of the collected sample.
*/
captureAtLeastOneJsSample() {
function getProfileSampleCount() {
const profile = Services.profiler.getProfileData();
return profile.threads[0].samples.data.length;
}
const sampleCount = getProfileSampleCount();
// Create an infinite loop until a sample has been collected.
// eslint-disable-next-line no-constant-condition
while (true) {
if (sampleCount < getProfileSampleCount()) {
return sampleCount;
}
}
},
/**
* Verify that a given JSON string is compact - i.e. does not contain
* unexpected whitespace.
*
* @param {String} the JSON string to check
* @return {Bool} Whether the string is compact or not
*/
verifyJSONStringIsCompact(s) {
function isJSONWhitespace(c) {
return ["\n", "\r", " ", "\t"].includes(c);
}
const stateData = 0;
const stateString = 1;
const stateEscapedChar = 2;
let state = stateData;
for (let i = 0; i < s.length; ++i) {
let c = s[i];
switch (state) {
case stateData:
if (isJSONWhitespace(c)) {
Assert.ok(
false,
`"Unexpected JSON whitespace at index ${i} in profile: <<<${s}>>>"`
);
return;
}
if (c == '"') {
state = stateString;
}
break;
case stateString:
if (c == '"') {
state = stateData;
} else if (c == "\\") {
state = stateEscapedChar;
}
break;
case stateEscapedChar:
state = stateString;
break;
}
}
},
/**
* This function pauses the profiler before getting the profile. Then after
* getting the data, the profiler is stopped, and all profiler data is removed.
* @returns {Promise<Profile>}
*/
async stopNowAndGetProfile() {
// Don't await the pause, because each process will handle it before it
// receives the following `getProfileDataAsArrayBuffer()`.
Services.profiler.Pause();
const profileArrayBuffer =
await Services.profiler.getProfileDataAsArrayBuffer();
await Services.profiler.StopProfiler();
const profileUint8Array = new Uint8Array(profileArrayBuffer);
const textDecoder = new TextDecoder("utf-8", { fatal: true });
const profileString = textDecoder.decode(profileUint8Array);
this.verifyJSONStringIsCompact(profileString);
return JSON.parse(profileString);
},
/**
* This function ensures there's at least one sample, then pauses the profiler
* before getting the profile. Then after getting the data, the profiler is
* stopped, and all profiler data is removed.
* @returns {Promise<Profile>}
*/
async waitSamplingAndStopAndGetProfile() {
await Services.profiler.waitOnePeriodicSampling();
return this.stopNowAndGetProfile();
},
/**
* Verifies that a marker is an interval marker.
*
* @param {InflatedMarker} marker
* @returns {boolean}
*/
isIntervalMarker(inflatedMarker) {
return (
inflatedMarker.phase === 1 &&
typeof inflatedMarker.startTime === "number" &&
typeof inflatedMarker.endTime === "number"
);
},
/**
* @param {Profile} profile
* @returns {Thread[]}
*/
getThreads(profile) {
const threads = [];
function getThreadsRecursive(process) {
for (const thread of process.threads) {
threads.push(thread);
}
for (const subprocess of process.processes) {
getThreadsRecursive(subprocess);
}
}
getThreadsRecursive(profile);
return threads;
},
/**
* Find a specific marker schema from any process of a profile.
*
* @param {Profile} profile
* @param {string} name
* @returns {MarkerSchema}
*/
getSchema(profile, name) {
{
const schema = profile.meta.markerSchema.find(s => s.name === name);
if (schema) {
return schema;
}
}
for (const subprocess of profile.processes) {
const schema = subprocess.meta.markerSchema.find(s => s.name === name);
if (schema) {
return schema;
}
}
console.error("Parent process schema", profile.meta.markerSchema);
for (const subprocess of profile.processes) {
console.error("Child process schema", subprocess.meta.markerSchema);
}
throw new Error(`Could not find a schema for "${name}".`);
},
};