/* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Test that ChromeUtils.addProfilerMarker is working correctly.
*/
const markerNamePrefix =
"test_addProfilerMarker";
const markerText =
"Text payload";
// The same startTime will be used for all markers with a duration,
// and we store this value globally so that expectDuration and
// expectNoDuration can access it. The value isn't set here as we
// want a start time after the profiler has started
var startTime;
function expectNoDuration(marker) {
Assert.equal(
typeof marker.startTime,
"number",
"startTime should be a number"
);
Assert.greater(
marker.startTime,
startTime,
"startTime should be after the begining of the test"
);
Assert.equal(
typeof marker.endTime,
"number",
"endTime should be a number");
Assert.equal(marker.endTime,
0,
"endTime should be 0");
}
function expectDuration(marker) {
Assert.equal(
typeof marker.startTime,
"number",
"startTime should be a number"
);
// Floats can cause rounding issues.
// In intermittent failures, we've seen up to a 9e-5 difference on win
// and 4.17e-5 on macosx, so we are permissive and accept up to 1e-4.
Assert.less(
Math.abs(marker.startTime - startTime),
1e-
4,
"startTime should be the expected time"
);
Assert.equal(
typeof marker.endTime,
"number",
"endTime should be a number");
Assert.greater(
marker.endTime,
startTime,
"endTime should be after startTime"
);
}
function expectNoData(marker) {
Assert.equal(
typeof marker.data,
"undefined",
"The data property should be undefined"
);
}
function expectText(marker) {
Assert.equal(
typeof marker.data,
"object",
"The data property should be an object"
);
Assert.equal(marker.data.type,
"Text",
"Should be a Text marker");
Assert.equal(
marker.data.name,
markerText,
"The payload should contain the expected text"
);
}
function expectNoStack(marker) {
Assert.ok(!marker.data || !marker.data.stack,
"There should be no stack");
}
function expectStack(marker,
thread) {
let stack = marker.data.stack;
Assert.ok(!!stack,
"There should be a stack");
// Marker stacks are recorded as a profile of a thread with a single sample,
// get the stack id.
stack = stack.samples.data[
0][stack.samples.schema.stack];
const stackPrefixCol =
thread.stackTable.schema.prefix;
const stackFrameCol =
thread.stackTable.schema.frame;
const frameLocationCol =
thread.frameTable.schema.location;
// Get the entire stack in an array for easier processing.
let result = [];
while (stack !=
null) {
let stackEntry =
thread.stackTable.data[stack];
let frame =
thread.frameTable.data[stackEntry[stackFrameCol]];
result.push(
thread.stringTable[frame[frameLocationCol]]);
stack = stackEntry[stackPrefixCol];
}
Assert.greaterOrEqual(
result.length,
1,
"There should be at least one frame in the stack"
);
Assert.ok(
result.some(frame => frame.includes(
"testMarker")),
"the 'testMarker' function should be visible in the stack"
);
Assert.ok(
!result.some(frame => frame.includes(
"ChromeUtils.addProfilerMarker")),
"the 'ChromeUtils.addProfilerMarker' label frame should not be visible in the stack"
);
}
add_task(async () => {
await ProfilerTestUtils.startProfilerForMarkerTests();
startTime = ChromeUtils.now();
while (ChromeUtils.now() < startTime +
1) {
// Busy wait for 1ms to ensure the intentionally set start time of markers
// will be significantly different from the time at which the marker is
// recorded.
}
info(
"startTime used for markers with durations: " + startTime);
/* Each call to testMarker will record a marker with a unique name.
* The testFunctions and testCases objects contain respectively test
* functions to verify that the marker found in the captured profile
* matches expectations, and a string that can be printed to describe
* in which way ChromeUtils.addProfilerMarker was called. */
let testFunctions = {};
let testCases = {};
let markerId =
0;
function testMarker(args, checks) {
let name = markerNamePrefix + markerId++;
ChromeUtils.addProfilerMarker(name, ...args);
testFunctions[name] = checks;
testCases[name] = `ChromeUtils.addProfilerMarker(${[name, ...args]
.toSource()
.slice(
1, -
1)})`;
}
info(
"Record markers without options object.");
testMarker([], m => {
expectNoDuration(m);
expectNoData(m);
});
testMarker([startTime], m => {
expectDuration(m);
expectNoData(m);
});
testMarker([undefined, markerText], m => {
expectNoDuration(m);
expectText(m);
});
testMarker([startTime, markerText], m => {
expectDuration(m);
expectText(m);
});
info(
"Record markers providing the duration as the startTime property.");
testMarker([{ startTime }], m => {
expectDuration(m);
expectNoData(m);
});
testMarker([{}, markerText], m => {
expectNoDuration(m);
expectText(m);
});
testMarker([{ startTime }, markerText], m => {
expectDuration(m);
expectText(m);
});
info(
"Record markers to test the captureStack property.");
const captureStack =
true;
testMarker([], expectNoStack);
testMarker([startTime, markerText], expectNoStack);
testMarker([{ captureStack:
false }], expectNoStack);
testMarker([{ captureStack }], expectStack);
testMarker([{ startTime, captureStack }], expectStack);
testMarker([{ captureStack }, markerText], expectStack);
testMarker([{ startTime, captureStack }, markerText], expectStack);
info(
"Record markers to test the category property");
function testCategory(args, expectedCategory) {
testMarker(args, marker => {
Assert.equal(marker.category, expectedCategory);
});
}
testCategory([],
"JavaScript");
testCategory([{ category:
"Test" }],
"Test");
testCategory([{ category:
"Test" }, markerText],
"Test");
testCategory([{ category:
"JavaScript" }],
"JavaScript");
testCategory([{ category:
"Other" }],
"Other");
testCategory([{ category:
"DOM" }],
"DOM");
testCategory([{ category:
"does not exist" }],
"Other");
info(
"Capture the profile");
const profile = await ProfilerTestUtils.stopNowAndGetProfile();
const mainThread = profile.threads.find(({ name }) => name ===
"GeckoMain");
const markers = ProfilerTestUtils.getInflatedMarkerData(mainThread).filter(
m => m.name.startsWith(markerNamePrefix)
);
Assert.equal(
markers.length,
Object.keys(testFunctions).length,
`Found ${markers.length} test markers in the captured profile`
);
for (let marker of markers) {
marker.category = profile.meta.categories[marker.category].name;
info(`${testCases[marker.name]} -> ${marker.toSource()}`);
testFunctions[marker.name](marker, mainThread);
delete testFunctions[marker.name];
}
Assert.equal(
0, Object.keys(testFunctions).length,
"all markers were found");
});
add_task(async
function test_registerMarkerSchema() {
const schema = {
name:
"CustomMarker",
display: [
"marker-chart",
"marker-table"],
data: [
{ key:
"field1", label:
"Field 1", format:
"string" },
{ key:
"field2", label:
"Field 2", format:
"integer" },
],
};
ChromeUtils.registerMarkerSchema(schema);
Assert.ok(
true,
"Schema registered without error");
});
add_task(async
function test_registerMarkerSchemaNoName() {
const invalidSchema = {
display: [
"marker-chart"],
data: [],
};
Assert.
throws(
() => ChromeUtils.registerMarkerSchema(invalidSchema),
/name/,
"Should throw when schema lacks 'name' field"
);
});
add_task(async
function test_addMarkerWithObject() {
await ProfilerTestUtils.startProfilerForMarkerTests();
const markerData = {
type:
"CustomMarker",
field1:
"test value",
field2:
42,
};
ChromeUtils.addProfilerMarker(
"TestMarker", {}, markerData);
const profile = await ProfilerTestUtils.stopNowAndGetProfile();
dump(JSON.stringify(profile) +
"\n");
const mainThread = profile.threads.find(({ name }) => name ===
"GeckoMain");
const markers = ProfilerTestUtils.getInflatedMarkerData(mainThread);
const customMarker = markers.find(m => m.name ===
"TestMarker");
Assert.ok(customMarker,
"Custom marker found in profile");
Assert.ok(customMarker.data,
"Marker has data");
Assert.deepEqual(
customMarker.data,
markerData,
"Marker data should match exactly what was passed in"
);
const schemas = profile.meta.markerSchema;
const customSchema = schemas.find(s => s.name ===
"CustomMarker");
Assert.ok(customSchema,
"Custom schema found in profile");
});
add_task(async
function test_addMarkerWithCrossRealmPlainObject() {
// A plain object coming from a different realm reaches AddProfilerMarker as
// a cross-compartment wrapper. JS::GetBuiltinClass unwraps proxies, but
// JS::ToJSONMaybeSafely's assertion checks the wrapper directly, so we need
// to unwrap before calling it.
await ProfilerTestUtils.startProfilerForMarkerTests();
const sandbox = Cu.Sandbox(Cu.getGlobalForObject(Services), {
wantGlobalProperties: [
"ChromeUtils"],
});
const wrappedObject = Cu.evalInSandbox(
"({ type: 'CustomMarker', field1: 'crossrealm', field2: 7 })",
sandbox
);
ChromeUtils.addProfilerMarker(
"CrossRealmMarker", {}, wrappedObject);
const profile = await ProfilerTestUtils.stopNowAndGetProfile();
const mainThread = profile.threads.find(({ name }) => name ===
"GeckoMain");
const markers = ProfilerTestUtils.getInflatedMarkerData(mainThread);
const marker = markers.find(m => m.name ===
"CrossRealmMarker");
Assert.ok(marker,
"Marker with cross-realm object data was recorded");
Assert.deepEqual(
marker.data,
{ type:
"CustomMarker", field1:
"crossrealm", field2:
7 },
"Marker data should match the cross-realm plain object"
);
});
add_task(async
function test_addMarkerWithNonPlainObject() {
// Non-plain objects (e.g. Error, Array) passed as the data argument should
// be recorded as text markers using the object's string form.
await ProfilerTestUtils.startProfilerForMarkerTests();
const err =
new TypeError(
"boom");
ChromeUtils.addProfilerMarker(
"NonPlainObjectMarker", {}, err);
ChromeUtils.addProfilerMarker(
"ArrayMarker", {}, [
1,
2,
3]);
const profile = await ProfilerTestUtils.stopNowAndGetProfile();
const mainThread = profile.threads.find(({ name }) => name ===
"GeckoMain");
const markers = ProfilerTestUtils.getInflatedMarkerData(mainThread);
const errMarker = markers.find(m => m.name ===
"NonPlainObjectMarker");
Assert.ok(errMarker,
"Marker with Error data was recorded");
Assert.equal(errMarker.data.type,
"Text",
"Should fall back to Text marker");
Assert.equal(
errMarker.data.name,
String(err),
"Text payload should be the Error's string form"
);
const arrMarker = markers.find(m => m.name ===
"ArrayMarker");
Assert.ok(arrMarker,
"Marker with Array data was recorded");
Assert.equal(arrMarker.data.type,
"Text",
"Should fall back to Text marker");
Assert.equal(
arrMarker.data.name,
"1,2,3",
"Text payload should be the Array's string form"
);
});