Bug 1911021 - Cover JavaScript tracing via the profiler with a mochitest. r=profiler-reviewers,aabh

Differential Revision: https://phabricator.services.mozilla.com/D220849
This commit is contained in:
Alexandre Poirot 2024-10-15 18:13:41 +00:00
parent 9a9ec02fd6
commit e8009bc6c7
3 changed files with 246 additions and 0 deletions

View File

@ -11,6 +11,13 @@ support-files = ["simple.html"]
["browser_test_feature_jsallocations.js"]
support-files = ["do_work_500ms.html"]
["browser_test_feature_jstracing.js"]
support-files = ["tracing.html"]
skip-if = [
# This features is only enabled on nightly via MOZ_EXECUTION_TRACING build flag
"!nightly_build"
]
["browser_test_feature_multiprocess_capture_with_signal.js"]
support-files = ["do_work_500ms.html"]
skip-if = [

View File

@ -0,0 +1,215 @@
/* 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 the JS Tracing feature.
*/
add_task(async function test_profile_feature_jstracing() {
Assert.ok(
!Services.profiler.IsActive(),
"The profiler is not currently active"
);
await startProfiler({ features: ["tracing"] });
const url = BASE_URL_HTTPS + "tracing.html";
await BrowserTestUtils.withNewTab(url, async contentBrowser => {
const contentPid = await SpecialPowers.spawn(
contentBrowser,
[],
() => Services.appinfo.processID
);
{
const { contentThread } = await stopProfilerNowAndGetThreads(contentPid);
// First lookup for all our expected symbols in the string table
const functionAFrameStringIdx = contentThread.stringTable.indexOf(
`a (${url}:7:15)`
);
Assert.greater(
functionAFrameStringIdx,
0,
"Found string for 'a' method call"
);
const functionBFrameStringIdx = contentThread.stringTable.indexOf(
`b (${url}:10:15)`
);
Assert.greater(
functionBFrameStringIdx,
0,
"Found string for 'b' method call"
);
const clickEventStringIdx = contentThread.stringTable.indexOf(`click`);
Assert.greater(
clickEventStringIdx,
0,
"Found string for 'click' DOM event"
);
const customEventStringIdx =
contentThread.stringTable.indexOf(`CustomEvent`);
Assert.greater(
customEventStringIdx,
0,
"Found string for 'CustomEvent' DOM event"
);
const customEventHandlerStringIdx = contentThread.stringTable.indexOf(
`customEventHandler (${url}:18:71)`
);
Assert.greater(
customEventHandlerStringIdx,
0,
"Found string for 'customEventHandler' method call"
);
// Then lookup for the matching frame, based on the string index
const { frameTable } = contentThread;
const FRAME_LOCATION_SLOT = frameTable.schema.location;
const FRAME_CATEGORY_SLOT = frameTable.schema.category;
const FUNCTION_CALL_CATEGORY = 4;
const EVENT_CATEGORY = 8;
const functionAFrameIdx = frameTable.data.findIndex(
frame => frame[FRAME_LOCATION_SLOT] == functionAFrameStringIdx
);
Assert.greater(functionAFrameIdx, 0, "Found frame for 'a' method call");
Assert.equal(
frameTable.data[functionAFrameIdx][FRAME_CATEGORY_SLOT],
FUNCTION_CALL_CATEGORY
);
const functionBFrameIdx = frameTable.data.findIndex(
frame => frame[FRAME_LOCATION_SLOT] == functionBFrameStringIdx
);
Assert.greater(functionBFrameIdx, 0, "Found frame for 'b' method call");
Assert.equal(
frameTable.data[functionBFrameIdx][FRAME_CATEGORY_SLOT],
FUNCTION_CALL_CATEGORY
);
const clickEventFrameIdx = frameTable.data.findIndex(
frame => frame[FRAME_LOCATION_SLOT] == clickEventStringIdx
);
Assert.greater(
clickEventFrameIdx,
0,
"Found frame for 'click' DOM event"
);
Assert.equal(
frameTable.data[clickEventFrameIdx][FRAME_CATEGORY_SLOT],
EVENT_CATEGORY
);
const customEventFrameIdx = frameTable.data.findIndex(
frame => frame[FRAME_LOCATION_SLOT] == customEventStringIdx
);
Assert.greater(
customEventFrameIdx,
0,
"Found frame for 'CustomEvent' DOM event"
);
Assert.equal(
frameTable.data[customEventFrameIdx][FRAME_CATEGORY_SLOT],
EVENT_CATEGORY
);
const customEventHandlerFrameIdx = frameTable.data.findIndex(
frame => frame[FRAME_LOCATION_SLOT] == customEventHandlerStringIdx
);
Assert.greater(
customEventHandlerFrameIdx,
0,
"Found frame for 'b' method call"
);
Assert.equal(
frameTable.data[customEventHandlerFrameIdx][FRAME_CATEGORY_SLOT],
FUNCTION_CALL_CATEGORY
);
// Finally, assert that the stacks are correct.
// Each symbol's frame is visible in a stack, and the stack tree is valid
const { stackTable } = contentThread;
const STACK_FRAME_SLOT = stackTable.schema.frame;
const STACK_PREFIX_SLOT = stackTable.schema.prefix;
const functionAFrame = stackTable.data.find(
stack => stack[STACK_FRAME_SLOT] == functionAFrameIdx
);
const functionBFrame = stackTable.data.find(
stack => stack[STACK_FRAME_SLOT] == functionBFrameIdx
);
const clickEventFrame = stackTable.data.find(
stack => stack[STACK_FRAME_SLOT] == clickEventFrameIdx
);
const customEventFrame = stackTable.data.find(
stack => stack[STACK_FRAME_SLOT] == customEventFrameIdx
);
const customEventHandlerEventFrame = stackTable.data.find(
stack => stack[STACK_FRAME_SLOT] == customEventHandlerFrameIdx
);
Assert.equal(
getCallerNameFromStackIdx(
contentThread,
functionAFrame[STACK_PREFIX_SLOT]
),
"load",
"'a' was called from 'load'"
);
Assert.equal(
functionBFrame[STACK_PREFIX_SLOT],
stackTable.data.indexOf(functionAFrame),
"'b' was called from 'a'"
);
Assert.equal(
clickEventFrame[STACK_PREFIX_SLOT],
stackTable.data.indexOf(functionBFrame),
"'click' event fired from 'b()' method call"
);
Assert.equal(
customEventFrame[STACK_PREFIX_SLOT],
stackTable.data.indexOf(functionBFrame),
"'CustomEvent' event fired from 'b()' method call"
);
Assert.equal(
customEventHandlerEventFrame[STACK_PREFIX_SLOT],
stackTable.data.indexOf(customEventFrame),
"'customEventHandler' function is called because of the CustomEvent Event"
);
}
});
});
function getCallerNameFromStackIdx(thread, stackIdx) {
const { stackTable, frameTable } = thread;
const frameIdx = stackTable.data[stackIdx][stackTable.schema.frame];
return thread.stringTable[
frameTable.data[frameIdx][frameTable.schema.location]
];
}
/**
* This function takes a thread, and a sample tuple from the "data" array, and
* inflates the frame to be an array of strings.
*
* @param {Object} thread - The thread from the profile.
* @param {Array} sample - The tuple from the thread.samples.data array.
* @returns {Array<string>} An array of function names.
*/
function getInflatedStackLocations(thread, sample) {
let stackTable = thread.stackTable;
let frameTable = thread.frameTable;
let stringTable = thread.stringTable;
let SAMPLE_STACK_SLOT = thread.samples.schema.stack;
let STACK_PREFIX_SLOT = stackTable.schema.prefix;
let STACK_FRAME_SLOT = stackTable.schema.frame;
let FRAME_LOCATION_SLOT = frameTable.schema.location;
// Build the stack from the raw data and accumulate the locations in
// an array.
let stackIndex = sample[SAMPLE_STACK_SLOT];
let locations = [];
while (stackIndex !== null) {
let stackEntry = stackTable.data[stackIndex];
let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
stackIndex = stackEntry[STACK_PREFIX_SLOT];
}
// The profiler tree is inverted, so reverse the array.
return locations.reverse();
}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Do some naive JS executions</title>
<script>
function a() {
b();
}
function b() {
document.body.setAttribute("foo", "bar");
navigator.userAgent;
window.dispatchEvent(new Event("CustomEvent"));
document.body.click();
}
window.onload = a;
window.onclick = () => console.log("click!");
window.addEventListener("CustomEvent", function customEventHandler() {});
</script>
</head>
<body>
Execute some naive code which should be traced.
</body>
</html>