Skip to content

Instantly share code, notes, and snippets.

@codehag
Forked from ochameau/compare-profile-alex.js
Last active September 30, 2023 03:38
Show Gist options
  • Select an option

  • Save codehag/466a069158099cd28b244b94377f4d20 to your computer and use it in GitHub Desktop.

Select an option

Save codehag/466a069158099cd28b244b94377f4d20 to your computer and use it in GitHub Desktop.
var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Components.utils.import("resource://gre/modules/osfile.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
var done = false;
let gPaths = arguments.filter(a => !a.startsWith("-"));
if (!gPaths.length) {
gPaths = [];
}
function getMarkerTime(thread, name) {
for (let [,,marker] of thread.markers.data) {
if (!marker || !marker.name) continue;
if (marker.name == name)
return {
start: marker.startTime,
end: marker.endTime,
};
}
throw "marker not found: "+marker;
}
const decoder = new TextDecoder();
async function readProfile(path) {
const data = decoder.decode(await OS.File.read(path));
print("parsed " + path);
return JSON.parse(data);
}
/** EDIT THIS LINE **/
const markerName = "custom.jsdebugger.stepOut.DAMP";
(async function() {
let currentDirectory = await OS.File.getCurrentDirectory();
let paths = gPaths.map(a => currentDirectory + "/" + a);
if (!paths.length) {
print("error, no path provided");
done = true;
return;
}
if (paths.length != 2) {
print("need exactly two profiles, got:" + paths.length);
done = true
return;
}
let json = await readProfile(paths[0]);
let json2 = await readProfile(paths[1]);
json.threads = json.threads.filter(t => t.name == "GeckoMain");
json2.threads = json2.threads.filter(t => t.name == "GeckoMain");
json.threads.push(json2.threads[0]);
for (let thread of json.threads) {
let { start, end } = getMarkerTime(thread, markerName);
let samples = thread.samples;
let timeCol = samples.schema.time;
samples.data = samples.data.filter(s => {
let t = s[timeCol];
return t >= start && t <= end;
});
for (let d of samples.data)
d[timeCol] -= start;
thread.registerTime = 0;
let markers = thread.markers;
timeCol = markers.schema.time;
markers.data = markers.data.filter(m => m[timeCol] >= start);
for (let m of markers.data)
m[timeCol] -= start;
}
json.processes = [];
let threads = json.threads;
threads[0].name = "First profile";
threads[1].name = "Second profile";
threads.push(JSON.parse(JSON.stringify(threads[0])));
threads[2].name = "Only first";
threads.push(JSON.parse(JSON.stringify(threads[1])));
threads[3].name = "Only second";
let onlyMax = threads[2];
let onlyMin = threads[3];
function getJSStack(thread, sample) {
let result = [];
let stackCol = thread.samples.schema.stack;
let stackPrefixCol = thread.stackTable.schema.prefix;
let stackFrameCol = thread.stackTable.schema.frame;
let frameLocationCol = thread.frameTable.schema.location;
let frameImplementationCol = thread.frameTable.schema.implementation;
let stackId = sample[stackCol];
while (stackId !== null) {
let stack = thread.stackTable.data[stackId];
let frame = thread.frameTable.data[stack[stackFrameCol]];
if (typeof frame[frameImplementationCol] == "number")
result.push(thread.stringTable[frame[frameLocationCol]]);
stackId = stack[stackPrefixCol];
}
if (result.length > 10)
result = result.slice(0, 10);
return JSON.stringify(result);
}
function getStack(thread, sample) {
let result = [];
let jsresult = [];
let stackCol = thread.samples.schema.stack;
let stackPrefixCol = thread.stackTable.schema.prefix;
let stackFrameCol = thread.stackTable.schema.frame;
let frameLocationCol = thread.frameTable.schema.location;
let frameImplementationCol = thread.frameTable.schema.implementation;
let stackId = sample[stackCol];
while (stackId !== null) {
let stack = thread.stackTable.data[stackId];
let frame = thread.frameTable.data[stack[stackFrameCol]];
let line = thread.stringTable[frame[frameLocationCol]];
if (line.includes(".js") || line.includes(".xml"))
jsresult.push(line);
result.push(line);
stackId = stack[stackPrefixCol];
}
if (jsresult.length > 2) {
result = jsresult;
// print(jsresult.toSource());
}
if (result.length > 10)
result = result.slice(0, 10);
return JSON.stringify(result).replace(/0x[0-9a-f]{8}/g, "address");
}
function getStackMap(thread) {
let stackMap = new Map();
for (let sample of thread.samples.data) {
let stack = getStack(thread, sample);
stackMap.set(stack, (stackMap.get(stack) || 0) + 1);
}
return stackMap;
}
let maxMap = getStackMap(onlyMax);
let minMap = getStackMap(onlyMin);
print("maxMap.size = " + maxMap.size);
print("minMap.size = " + minMap.size);
print("onlyMax samples before: " + onlyMax.samples.data.length);
onlyMax.samples.data = onlyMax.samples.data.filter(sample => {
let stack = getStack(onlyMax, sample);
let count = minMap.get(stack);
if (count) {
if (count == 1)
minMap.delete(stack);
else
minMap.set(stack, count - 1);
}
return !count;
});
print("onlyMax samples after: " + onlyMax.samples.data.length);
print("onlyMin samples before: " + onlyMin.samples.data.length);
onlyMin.samples.data = onlyMin.samples.data.filter(sample => {
let stack = getStack(onlyMin, sample);
let count = maxMap.get(stack);
if (count) {
if (count == 1)
maxMap.delete(stack);
else
maxMap.set(stack, count - 1);
}
return !count;
});
print("onlyMin samples after: " + onlyMin.samples.data.length);
/*
for (let [stack, count] of maxMap) {
print(stack);
}
*/
await OS.File.writeAtomic("merged-profile.json", (new TextEncoder()).encode(JSON.stringify(json)));
done = true;
})().catch(e => {
dump(e + "\n");
done = true;
});
// Spin an event loop.
(() => {
var thread = Components.classes["@mozilla.org/thread-manager;1"]
.getService().currentThread;
while (!done)
thread.processNextEvent(true);
// get rid of any pending event
while (thread.hasPendingEvents())
thread.processNextEvent(true);
})();
@codehag
Copy link
Author

codehag commented Nov 9, 2018

Alex's comment:

Florian did an experimental script to help comparing two profiles, I tweaked it to accomodate my usecase and I think that's a very good starting point that help prototype how we could compare stacks and frames!

Here is my fork, it is run like this:
$OBJDIR/dist/bin/run-mozilla.sh $OBJDIR/dist/bin/xpcshell compare-profile-alex.js p1.profile p2.profile
You can omit run-mozilla.sh thing if you are not on linux. The two profiles as arguments have to be Talos profile fetched directly from Talos, like this talos zip file.

This script does three things:

  • look for a marker (its name is hardcoded in the js file, you will want to modify it if you want to test it!), and only care about the data within marker timeline,
  • aggregate two profiles in one, thus, helping comparing two from the same interface and so reuse the same filter, that is handy!
  • compute two new fake thread, one being all the stacks that are in the first profile and not in the second and another thread with the other way around.

All the credits go to florian, his original script supports profile fetched from perf-html server rather than the one from talos and accept a folder as argument. It will filter the slowest and the fastest profile and compare them. It also contains hardcoded markers strings in it you will want to modify.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment