summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/gecko-profile-collector.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/utils/gecko-profile-collector.js')
-rw-r--r--devtools/server/actors/utils/gecko-profile-collector.js285
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;