diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/utils/gecko-profile-collector.js | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/gecko-profile-collector.js b/devtools/server/actors/utils/gecko-profile-collector.js new file mode 100644 index 0000000000..1cdb6d7e56 --- /dev/null +++ b/devtools/server/actors/utils/gecko-profile-collector.js @@ -0,0 +1,285 @@ +/* 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/firefox-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; |