285 lines
7.6 KiB
JavaScript
285 lines
7.6 KiB
JavaScript
/* 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)`
|
|
// https://github.com/icecat-devtools/profiler/blob/dab645b2db7e1b21185b286f96dd03b77f68f5c3/src/profile-logic/process-profile.js#L518
|
|
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;
|