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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// The fallback color for unexpected cases
const DEFAULT_COLOR = "grey";
// The default category for unexpected cases
const DEFAULT_CATEGORIES = [
{
name: "Mixed",
color: DEFAULT_COLOR,
subcategories: ["Other"],
},
];
// Color for each type of category/frame's implementation
const PREDEFINED_COLORS = {
interpreter: "yellow",
baseline: "orange",
ion: "blue",
wasm: "purple",
};
/**
* Utility class that collects the JS tracer data and converts it to a Gecko
* profile object.
*/
class GeckoProfileCollector {
#thread = null;
#stackMap = new Map();
#frameMap = new Map();
#categories = DEFAULT_CATEGORIES;
#currentStack = [];
#time = 0;
/**
* Initialize the profiler and be ready to receive samples.
*/
start() {
this.#reset();
this.#thread = this.#getEmptyThread();
}
/**
* Stop the record and return the gecko profiler data.
*
* @return {Object}
* The Gecko profile object.
*/
stop() {
// Create the profile to return.
const profile = this.#getEmptyProfile();
profile.meta.categories = this.#categories;
profile.threads.push(this.#thread);
// Cleanup.
this.#reset();
return profile;
}
/**
* Clear all the internal state of this class.
*/
#reset() {
this.#thread = null;
this.#stackMap = new Map();
this.#frameMap = new Map();
this.#categories = DEFAULT_CATEGORIES;
this.#currentStack = [];
this.#time = 0;
}
/**
* Initialize an empty Gecko profile object.
*
* @return {Object}
* Gecko profile object.
*/
#getEmptyProfile() {
const httpHandler = Cc[
"@mozilla.org/network/protocol;1?name=http"
].getService(Ci.nsIHttpProtocolHandler);
return {
meta: {
// Currently interval is 1, but we could change it to a lower number
// when we have durations coming from js tracer.
interval: 1,
startTime: 0,
product: Services.appinfo.name,
importedFrom: "JS Tracer",
version: 28,
presymbolicated: true,
abi: Services.appinfo.XPCOMABI,
misc: httpHandler.misc,
oscpu: httpHandler.oscpu,
platform: httpHandler.platform,
processType: Services.appinfo.processType,
categories: [],
stackwalk: 0,
toolkit: Services.appinfo.widgetToolkit,
appBuildID: Services.appinfo.appBuildID,
sourceURL: Services.appinfo.sourceURL,
physicalCPUs: 0,
logicalCPUs: 0,
CPUName: "",
markerSchema: [],
},
libs: [],
pages: [],
threads: [],
processes: [],
};
}
/**
* Generate a thread object to be stored in the Gecko profile object.
*/
#getEmptyThread() {
return {
processType: "default",
processStartupTime: 0,
processShutdownTime: null,
registerTime: 0,
unregisterTime: null,
pausedRanges: [],
name: "GeckoMain",
"eTLD+1": "JS Tracer",
isMainThread: true,
pid: Services.appinfo.processID,
tid: 0,
samples: {
schema: {
stack: 0,
time: 1,
eventDelay: 2,
},
data: [],
},
markers: {
schema: {
name: 0,
startTime: 1,
endTime: 2,
phase: 3,
category: 4,
data: 5,
},
data: [],
},
stackTable: {
schema: {
prefix: 0,
frame: 1,
},
data: [],
},
frameTable: {
schema: {
location: 0,
relevantForJS: 1,
innerWindowID: 2,
implementation: 3,
line: 4,
column: 5,
category: 6,
subcategory: 7,
},
data: [],
},
stringTable: [],
};
}
/**
* Record a new sample to be stored in the Gecko profile object.
*
* @param {Object} frame
* Object describing a frame with following attributes:
* - {String} name
* Human readable name for this frame.
* - {String} url
* URL of the running script.
* - {Number} lineNumber
* Line currently executing for this script.
* - {Number} columnNumber
* Column currently executing for this script.
* - {String} category
* Which JS implementation is being used for this frame: interpreter, baseline, ion or wasm.
* See Debugger.frame.implementation.
*/
addSample(frame, depth) {
const currentDepth = this.#currentStack.length;
if (currentDepth == depth) {
// We are in the same depth and executing another frame. Replace the
// current frame with the new one.
this.#currentStack[currentDepth] = frame;
} else if (currentDepth < depth) {
// We are going deeper in the stack. Push the new frame.
this.#currentStack.push(frame);
} else {
// We are going back in the stack. Pop frames until we reach the right depth.
this.#currentStack.length = depth;
this.#currentStack[depth] = frame;
}
const stack = this.#currentStack.reduce((prefix, stackFrame) => {
const frameIndex = this.#getOrCreateFrame(stackFrame);
return this.#getOrCreateStack(frameIndex, prefix);
}, null);
this.#thread.samples.data.push([
stack,
// We put simply 1 sample (1ms) for each frame. We can change it in the
// future if we can get the duration of the frame.
this.#time++,
0, // eventDelay
]);
}
#getOrCreateFrame(frame) {
const { frameTable, stringTable } = this.#thread;
const frameString = `${frame.name}:${frame.url}:${frame.lineNumber}:${frame.columnNumber}:${frame.category}`;
let frameIndex = this.#frameMap.get(frameString);
if (frameIndex === undefined) {
frameIndex = frameTable.data.length;
const location = stringTable.length;
// Profiler frontend except a particular string to match the source URL:
// `functionName (http://script.url/:1234:1234)`
stringTable.push(
`${frame.name} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`
);
const category = this.#getOrCreateCategory(frame.category);
frameTable.data.push([
location,
true, // relevantForJS
0, // innerWindowID
null, // implementation
frame.lineNumber, // line
frame.columnNumber, // column
category,
0, // subcategory
]);
this.#frameMap.set(frameString, frameIndex);
}
return frameIndex;
}
#getOrCreateStack(frameIndex, prefix) {
const { stackTable } = this.#thread;
const key = prefix === null ? `${frameIndex}` : `${frameIndex},${prefix}`;
let stack = this.#stackMap.get(key);
if (stack === undefined) {
stack = stackTable.data.length;
stackTable.data.push([prefix, frameIndex]);
this.#stackMap.set(key, stack);
}
return stack;
}
#getOrCreateCategory(category) {
const categories = this.#categories;
let categoryIndex = categories.findIndex(c => c.name === category);
if (categoryIndex === -1) {
categoryIndex = categories.length;
categories.push({
name: category,
color: PREDEFINED_COLORS[category] ?? DEFAULT_COLOR,
subcategories: ["Other"],
});
}
return categoryIndex;
}
}
exports.GeckoProfileCollector = GeckoProfileCollector;