summaryrefslogtreecommitdiffstats
path: root/devtools/shared/performance
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/shared/performance-new/gecko-profiler-interface.js271
-rw-r--r--devtools/shared/performance-new/moz.build13
-rw-r--r--devtools/shared/performance-new/recording-utils.js63
-rw-r--r--devtools/shared/performance/moz.build12
-rw-r--r--devtools/shared/performance/recording-common.js141
-rw-r--r--devtools/shared/performance/recording-utils.js624
-rw-r--r--devtools/shared/performance/xpcshell/.eslintrc.js10
-rw-r--r--devtools/shared/performance/xpcshell/head.js8
-rw-r--r--devtools/shared/performance/xpcshell/test_perf-utils-allocations-to-samples.js97
-rw-r--r--devtools/shared/performance/xpcshell/xpcshell.ini7
10 files changed, 1246 insertions, 0 deletions
diff --git a/devtools/shared/performance-new/gecko-profiler-interface.js b/devtools/shared/performance-new/gecko-profiler-interface.js
new file mode 100644
index 0000000000..3b893a0385
--- /dev/null
+++ b/devtools/shared/performance-new/gecko-profiler-interface.js
@@ -0,0 +1,271 @@
+/* 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";
+
+/**
+ * This file is for the new performance panel that targets profiler.firefox.com,
+ * not the default-enabled DevTools performance panel.
+ */
+
+const { Ci } = require("chrome");
+const Services = require("Services");
+
+loader.lazyImporter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+loader.lazyRequireGetter(
+ this,
+ "RecordingUtils",
+ "devtools/shared/performance-new/recording-utils"
+);
+
+// Some platforms are built without the Gecko Profiler.
+const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
+
+/**
+ * The GeckoProfiler already has an interface to control it through the
+ * nsIProfiler component. However, this class implements an interface that can
+ * be used on both the actor, and the profiler popup. This allows us to share
+ * the UI for the devtools front-end and the profiler popup code. The devtools
+ * code needs to work through the actor system, while the popup code controls
+ * the Gecko Profiler on the current browser.
+ */
+class ActorReadyGeckoProfilerInterface {
+ /**
+ * @param {Object} options
+ * @param options.gzipped - This flag controls whether or not to gzip the profile when
+ * capturing it. The profiler popup wants a gzipped profile in an array buffer, while
+ * the devtools want the full object. See Bug 1581963 to perhaps provide an API
+ * to request the gzipped profile. This would then remove this configuration from
+ * the GeckoProfilerInterface.
+ */
+ constructor(
+ options = {
+ gzipped: true,
+ }
+ ) {
+ // Only setup the observers on a supported platform.
+ if (IS_SUPPORTED_PLATFORM) {
+ this._observer = {
+ observe: this._observe.bind(this),
+ };
+ Services.obs.addObserver(this._observer, "profiler-started");
+ Services.obs.addObserver(this._observer, "profiler-stopped");
+ Services.obs.addObserver(
+ this._observer,
+ "chrome-document-global-created"
+ );
+ Services.obs.addObserver(this._observer, "last-pb-context-exited");
+ }
+ this.gzipped = options.gzipped;
+
+ EventEmitter.decorate(this);
+ }
+
+ destroy() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return;
+ }
+ Services.obs.removeObserver(this._observer, "profiler-started");
+ Services.obs.removeObserver(this._observer, "profiler-stopped");
+ Services.obs.removeObserver(
+ this._observer,
+ "chrome-document-global-created"
+ );
+ Services.obs.removeObserver(this._observer, "last-pb-context-exited");
+ }
+
+ startProfiler(options) {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+
+ // For a quick implementation, decide on some default values. These may need
+ // to be tweaked or made configurable as needed.
+ const settings = {
+ entries: options.entries || 1000000,
+ duration: options.duration || 0,
+ interval: options.interval || 1,
+ features: options.features || [
+ "js",
+ "stackwalk",
+ "responsiveness",
+ "threads",
+ "leaf",
+ ],
+ threads: options.threads || ["GeckoMain", "Compositor"],
+ activeBrowsingContextID: RecordingUtils.getActiveBrowsingContextID(),
+ };
+
+ try {
+ // This can throw an error if the profiler is in the wrong state.
+ Services.profiler.StartProfiler(
+ settings.entries,
+ settings.interval,
+ settings.features,
+ settings.threads,
+ settings.activeBrowsingContextID,
+ settings.duration
+ );
+ } catch (e) {
+ // In case any errors get triggered, bailout with a false.
+ return false;
+ }
+
+ return true;
+ }
+
+ stopProfilerAndDiscardProfile() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return;
+ }
+ Services.profiler.StopProfiler();
+ }
+
+ /**
+ * @type {string} debugPath
+ * @type {string} breakpadId
+ * @returns {Promise<[number[], number[], number[]]>}
+ */
+ async getSymbolTable(debugPath, breakpadId) {
+ const [addr, index, buffer] = await Services.profiler.getSymbolTable(
+ debugPath,
+ breakpadId
+ );
+ // The protocol does not support the transfer of typed arrays, so we convert
+ // these typed arrays to plain JS arrays of numbers now.
+ // Our return value type is declared as "array:array:number".
+ return [Array.from(addr), Array.from(index), Array.from(buffer)];
+ }
+
+ async getProfileAndStopProfiler() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return null;
+ }
+
+ // Pause profiler before we collect the profile, so that we don't capture
+ // more samples while the parent process or android threads wait for subprocess profiles.
+ Services.profiler.Pause();
+
+ let profile;
+ try {
+ // Attempt to pull out the data.
+ if (this.gzipped) {
+ profile = await Services.profiler.getProfileDataAsGzippedArrayBuffer();
+ } else {
+ profile = await Services.profiler.getProfileDataAsync();
+
+ if (Object.keys(profile).length === 0) {
+ console.error(
+ "An empty object was received from getProfileDataAsync.getProfileDataAsync(), " +
+ "meaning that a profile could not successfully be serialized and captured."
+ );
+ profile = null;
+ }
+ }
+ } catch (e) {
+ // Explicitly set the profile to null if there as an error.
+ profile = null;
+ console.error(
+ `There was an error fetching a profile (gzipped: ${this.gzipped})`,
+ e
+ );
+ }
+
+ // Stop and discard the buffers.
+ Services.profiler.StopProfiler();
+
+ // Returns a profile when successful, and null when there is an error.
+ return profile;
+ }
+
+ isActive() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+ return Services.profiler.IsActive();
+ }
+
+ isSupportedPlatform() {
+ return IS_SUPPORTED_PLATFORM;
+ }
+
+ isLockedForPrivateBrowsing() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return false;
+ }
+ return !Services.profiler.CanProfile();
+ }
+
+ /**
+ * Watch for events that happen within the browser. These can affect the
+ * current availability and state of the Gecko Profiler.
+ */
+ _observe(subject, topic, _data) {
+ // Note! If emitting new events make sure and update the list of bridged
+ // events in the perf actor.
+ switch (topic) {
+ case "chrome-document-global-created":
+ if (
+ subject.isChromeWindow &&
+ PrivateBrowsingUtils.isWindowPrivate(subject)
+ ) {
+ this.emit("profile-locked-by-private-browsing");
+ }
+ break;
+ case "last-pb-context-exited":
+ this.emit("profile-unlocked-from-private-browsing");
+ break;
+ case "profiler-started":
+ const param = subject.QueryInterface(Ci.nsIProfilerStartParams);
+ this.emit(
+ topic,
+ param.entries,
+ param.interval,
+ param.features,
+ param.duration,
+ param.activeBrowsingContextID
+ );
+ break;
+ case "profiler-stopped":
+ this.emit(topic);
+ break;
+ }
+ }
+
+ /**
+ * Lists the supported features of the profiler for the current browser.
+ * @returns {string[]}
+ */
+ getSupportedFeatures() {
+ if (!IS_SUPPORTED_PLATFORM) {
+ return [];
+ }
+ return Services.profiler.GetFeatures();
+ }
+
+ /**
+ * @param {string} type
+ * @param {() => void} listener
+ */
+ on(type, listener) {
+ // This is a stub for TypeScript. This function is assigned by the EventEmitter
+ // decorator.
+ }
+
+ /**
+ * @param {string} type
+ * @param {() => void} listener
+ */
+ off(type, listener) {
+ // This is a stub for TypeScript. This function is assigned by the EventEmitter
+ // decorator.
+ }
+}
+
+exports.ActorReadyGeckoProfilerInterface = ActorReadyGeckoProfilerInterface;
diff --git a/devtools/shared/performance-new/moz.build b/devtools/shared/performance-new/moz.build
new file mode 100644
index 0000000000..455098f4fb
--- /dev/null
+++ b/devtools/shared/performance-new/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "gecko-profiler-interface.js",
+ "recording-utils.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
diff --git a/devtools/shared/performance-new/recording-utils.js b/devtools/shared/performance-new/recording-utils.js
new file mode 100644
index 0000000000..bbae0449fd
--- /dev/null
+++ b/devtools/shared/performance-new/recording-utils.js
@@ -0,0 +1,63 @@
+/* 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/. */
+// @ts-check
+"use strict";
+
+/**
+ * This file is for the new performance panel that targets profiler.firefox.com,
+ * not the default-enabled DevTools performance panel.
+ */
+
+/**
+ * @typedef {import("../../client/performance-new/@types/perf").GetActiveBrowsingContextID} GetActiveBrowsingContextID
+ */
+
+/**
+ * TS-TODO
+ *
+ * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
+ * currently duplicated until we have consensus that TypeScript is a good idea.
+ *
+ * @template T
+ * @type {(callback: () => T) => () => T}
+ */
+function requireLazy(callback) {
+ /** @type {T | undefined} */
+ let cache;
+ return () => {
+ if (cache === undefined) {
+ cache = callback();
+ }
+ return cache;
+ };
+}
+
+const lazyServices = requireLazy(() =>
+ require("resource://gre/modules/Services.jsm")
+);
+
+/**
+ * Gets the ID of active BrowsingContext from the browser.
+ *
+ * @type {GetActiveBrowsingContextID}
+ */
+function getActiveBrowsingContextID() {
+ const { Services } = lazyServices();
+ const win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ if (win?.gBrowser?.selectedBrowser?.browsingContext?.id) {
+ return win.gBrowser.selectedBrowser.browsingContext.id;
+ }
+
+ console.error(
+ "Failed to get the active BrowsingContext ID while starting the profiler."
+ );
+ // `0` mean that we failed to ge the active BrowsingContext ID, and it's
+ // treated as null value in the platform.
+ return 0;
+}
+
+module.exports = {
+ getActiveBrowsingContextID,
+};
diff --git a/devtools/shared/performance/moz.build b/devtools/shared/performance/moz.build
new file mode 100644
index 0000000000..36f0139d6b
--- /dev/null
+++ b/devtools/shared/performance/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.ini"]
+
+DevToolsModules(
+ "recording-common.js",
+ "recording-utils.js",
+)
diff --git a/devtools/shared/performance/recording-common.js b/devtools/shared/performance/recording-common.js
new file mode 100644
index 0000000000..d0f1ae0c0d
--- /dev/null
+++ b/devtools/shared/performance/recording-common.js
@@ -0,0 +1,141 @@
+/* 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";
+
+/**
+ * A mixin to be used for PerformanceRecordingActor, PerformanceRecordingFront,
+ * and LegacyPerformanceRecording for helper methods to access data.
+ */
+
+exports.PerformanceRecordingCommon = {
+ // Private fields, only needed when a recording is started or stopped.
+ _console: false,
+ _imported: false,
+ _recording: false,
+ _completed: false,
+ _configuration: {},
+ _startingBufferStatus: null,
+ _localStartTime: 0,
+
+ // Serializable fields, necessary and sufficient for import and export.
+ _label: "",
+ _duration: 0,
+ _markers: null,
+ _frames: null,
+ _memory: null,
+ _ticks: null,
+ _allocations: null,
+ _profile: null,
+ _systemHost: null,
+ _systemClient: null,
+
+ /**
+ * Helper methods for returning the status of the recording.
+ * These methods should be consistent on both the front and actor.
+ */
+ isRecording: function() {
+ return this._recording;
+ },
+ isCompleted: function() {
+ return this._completed || this.isImported();
+ },
+ isFinalizing: function() {
+ return !this.isRecording() && !this.isCompleted();
+ },
+ isConsole: function() {
+ return this._console;
+ },
+ isImported: function() {
+ return this._imported;
+ },
+
+ /**
+ * Helper methods for returning configuration for the recording.
+ * These methods should be consistent on both the front and actor.
+ */
+ getConfiguration: function() {
+ return this._configuration;
+ },
+ getLabel: function() {
+ return this._label;
+ },
+
+ /**
+ * Gets duration of this recording, in milliseconds.
+ * @return number
+ */
+ getDuration: function() {
+ // Compute an approximate ending time for the current recording if it is
+ // still in progress. This is needed to ensure that the view updates even
+ // when new data is not being generated. If recording is completed, use
+ // the duration from the profiler; if between recording and being finalized,
+ // use the last estimated duration.
+ if (this.isRecording()) {
+ this._estimatedDuration = Date.now() - this._localStartTime;
+ return this._estimatedDuration;
+ }
+ return this._duration || this._estimatedDuration || 0;
+ },
+
+ /**
+ * Helper methods for returning recording data.
+ * These methods should be consistent on both the front and actor.
+ */
+ getMarkers: function() {
+ return this._markers;
+ },
+ getFrames: function() {
+ return this._frames;
+ },
+ getMemory: function() {
+ return this._memory;
+ },
+ getTicks: function() {
+ return this._ticks;
+ },
+ getAllocations: function() {
+ return this._allocations;
+ },
+ getProfile: function() {
+ return this._profile;
+ },
+ getHostSystemInfo: function() {
+ return this._systemHost;
+ },
+ getClientSystemInfo: function() {
+ return this._systemClient;
+ },
+ getStartingBufferStatus: function() {
+ return this._startingBufferStatus;
+ },
+
+ getAllData: function() {
+ const label = this.getLabel();
+ const duration = this.getDuration();
+ const markers = this.getMarkers();
+ const frames = this.getFrames();
+ const memory = this.getMemory();
+ const ticks = this.getTicks();
+ const allocations = this.getAllocations();
+ const profile = this.getProfile();
+ const configuration = this.getConfiguration();
+ const systemHost = this.getHostSystemInfo();
+ const systemClient = this.getClientSystemInfo();
+
+ return {
+ label,
+ duration,
+ markers,
+ frames,
+ memory,
+ ticks,
+ allocations,
+ profile,
+ configuration,
+ systemHost,
+ systemClient,
+ };
+ },
+};
diff --git a/devtools/shared/performance/recording-utils.js b/devtools/shared/performance/recording-utils.js
new file mode 100644
index 0000000000..dfc5110791
--- /dev/null
+++ b/devtools/shared/performance/recording-utils.js
@@ -0,0 +1,624 @@
+/* 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";
+
+/**
+ * Utility functions for managing recording models and their internal data,
+ * such as filtering profile samples or offsetting timestamps.
+ */
+
+function mapRecordingOptions(type, options) {
+ if (type === "profiler") {
+ return {
+ entries: options.bufferSize,
+ interval: options.sampleFrequency
+ ? 1000 / options.sampleFrequency
+ : void 0,
+ };
+ }
+
+ if (type === "memory") {
+ return {
+ probability: options.allocationsSampleProbability,
+ maxLogLength: options.allocationsMaxLogLength,
+ };
+ }
+
+ if (type === "timeline") {
+ return {
+ withMarkers: true,
+ withTicks: options.withTicks,
+ withMemory: options.withMemory,
+ withFrames: true,
+ withGCEvents: true,
+ withDocLoadingEvents: false,
+ };
+ }
+
+ return options;
+}
+
+/**
+ * Takes an options object for `startRecording`, and normalizes
+ * it based off of server support. For example, if the user
+ * requests to record memory `withMemory = true`, but the server does
+ * not support that feature, then the `false` will overwrite user preference
+ * in order to define the recording with what is actually available, not
+ * what the user initially requested.
+ *
+ * @param {object} options
+ * @param {boolean}
+ */
+function normalizePerformanceFeatures(options, supportedFeatures) {
+ return Object.keys(options).reduce((modifiedOptions, feature) => {
+ if (supportedFeatures[feature] !== false) {
+ modifiedOptions[feature] = options[feature];
+ }
+ return modifiedOptions;
+ }, Object.create(null));
+}
+
+/**
+ * Filters all the samples in the provided profiler data to be more recent
+ * than the specified start time.
+ *
+ * @param object profile
+ * The profiler data received from the backend.
+ * @param number profilerStartTime
+ * The earliest acceptable sample time (in milliseconds).
+ */
+function filterSamples(profile, profilerStartTime) {
+ const firstThread = profile.threads[0];
+ const TIME_SLOT = firstThread.samples.schema.time;
+ firstThread.samples.data = firstThread.samples.data.filter(e => {
+ return e[TIME_SLOT] >= profilerStartTime;
+ });
+}
+
+/**
+ * Offsets all the samples in the provided profiler data by the specified time.
+ *
+ * @param object profile
+ * The profiler data received from the backend.
+ * @param number timeOffset
+ * The amount of time to offset by (in milliseconds).
+ */
+function offsetSampleTimes(profile, timeOffset) {
+ const firstThread = profile.threads[0];
+ const TIME_SLOT = firstThread.samples.schema.time;
+ const samplesData = firstThread.samples.data;
+ for (let i = 0; i < samplesData.length; i++) {
+ samplesData[i][TIME_SLOT] -= timeOffset;
+ }
+}
+
+/**
+ * Offsets all the markers in the provided timeline data by the specified time.
+ *
+ * @param array markers
+ * The markers array received from the backend.
+ * @param number timeOffset
+ * The amount of time to offset by (in milliseconds).
+ */
+function offsetMarkerTimes(markers, timeOffset) {
+ for (const marker of markers) {
+ marker.start -= timeOffset;
+ marker.end -= timeOffset;
+ }
+}
+
+/**
+ * Offsets and scales all the timestamps in the provided array by the
+ * specified time and scale factor.
+ *
+ * @param array array
+ * A list of timestamps received from the backend.
+ * @param number timeOffset
+ * The amount of time to offset by (in milliseconds).
+ * @param number timeScale
+ * The factor to scale by, after offsetting.
+ */
+function offsetAndScaleTimestamps(timestamps, timeOffset, timeScale) {
+ for (let i = 0, len = timestamps.length; i < len; i++) {
+ timestamps[i] -= timeOffset;
+ if (timeScale) {
+ timestamps[i] /= timeScale;
+ }
+ }
+}
+
+/**
+ * Push all elements of src array into dest array. Marker data will come in small chunks
+ * and add up over time, whereas allocation arrays can be > 500000 elements (and
+ * Function.prototype.apply throws if applying more than 500000 elements, which
+ * is what spawned this separate function), so iterate one element at a time.
+ * @see bug 1166823
+ * @see http://jsperf.com/concat-large-arrays
+ * @see http://jsperf.com/concat-large-arrays/2
+ *
+ * @param {Array} dest
+ * @param {Array} src
+ */
+function pushAll(dest, src) {
+ const length = src.length;
+ for (let i = 0; i < length; i++) {
+ dest.push(src[i]);
+ }
+}
+
+/**
+ * Cache used in `RecordingUtils.getProfileThreadFromAllocations`.
+ */
+var gProfileThreadFromAllocationCache = new WeakMap();
+
+/**
+ * Converts allocation data from the memory actor to something that follows
+ * the same structure as the samples data received from the profiler.
+ *
+ * @see MemoryActor.prototype.getAllocations for more information.
+ *
+ * @param object allocations
+ * A list of { sites, timestamps, frames, sizes } arrays.
+ * @return object
+ * The "profile" describing the allocations log.
+ */
+function getProfileThreadFromAllocations(allocations) {
+ const cached = gProfileThreadFromAllocationCache.get(allocations);
+ if (cached) {
+ return cached;
+ }
+
+ const { sites, timestamps, frames, sizes } = allocations;
+ const uniqueStrings = new UniqueStrings();
+
+ // Convert allocation frames to the the stack and frame tables expected by
+ // the profiler format.
+ //
+ // Since the allocations log is already presented as a tree, we would be
+ // wasting time if we jumped through the same hoops as deflateProfile below
+ // and instead use the existing structure of the allocations log to build up
+ // the profile JSON.
+ //
+ // The allocations.frames array corresponds roughly to the profile stack
+ // table: a trie of all stacks. We could work harder to further deduplicate
+ // each individual frame as the profiler does, but it is not necessary for
+ // correctness.
+ const stackTable = new Array(frames.length);
+ const frameTable = new Array(frames.length);
+
+ // Array used to concat the location.
+ const locationConcatArray = new Array(5);
+
+ for (let i = 0; i < frames.length; i++) {
+ const frame = frames[i];
+ if (!frame) {
+ stackTable[i] = frameTable[i] = null;
+ continue;
+ }
+
+ const prefix = frame.parent;
+
+ // Schema:
+ // [prefix, frame]
+ stackTable[i] = [frames[prefix] ? prefix : null, i];
+
+ // Schema:
+ // [location]
+ //
+ // The only field a frame will have in an allocations profile is location.
+ //
+ // If frame.functionDisplayName is present, the format is
+ // "functionDisplayName (source:line:column)"
+ // Otherwise, it is
+ // "source:line:column"
+ //
+ // A static array is used to join to save memory on intermediate strings.
+ locationConcatArray[0] = frame.source;
+ locationConcatArray[1] = ":";
+ locationConcatArray[2] = String(frame.line);
+ locationConcatArray[3] = ":";
+ locationConcatArray[4] = String(frame.column);
+ locationConcatArray[5] = "";
+
+ let location = locationConcatArray.join("");
+ const funcName = frame.functionDisplayName;
+
+ if (funcName) {
+ locationConcatArray[0] = funcName;
+ locationConcatArray[1] = " (";
+ locationConcatArray[2] = location;
+ locationConcatArray[3] = ")";
+ locationConcatArray[4] = "";
+ locationConcatArray[5] = "";
+ location = locationConcatArray.join("");
+ }
+
+ frameTable[i] = [uniqueStrings.getOrAddStringIndex(location)];
+ }
+
+ const samples = new Array(sites.length);
+ let writePos = 0;
+ for (let i = 0; i < sites.length; i++) {
+ // Schema:
+ // [stack, time, size]
+ //
+ // Originally, sites[i] indexes into the frames array. Note that in the
+ // loop above, stackTable[sites[i]] and frames[sites[i]] index the same
+ // information.
+ const stackIndex = sites[i];
+ if (frames[stackIndex]) {
+ samples[writePos++] = [stackIndex, timestamps[i], sizes[i]];
+ }
+ }
+ samples.length = writePos;
+
+ const thread = {
+ name: "allocations",
+ samples: allocationsWithSchema(samples),
+ stackTable: stackTableWithSchema(stackTable),
+ frameTable: frameTableWithSchema(frameTable),
+ stringTable: uniqueStrings.stringTable,
+ };
+
+ gProfileThreadFromAllocationCache.set(allocations, thread);
+ return thread;
+}
+
+function allocationsWithSchema(data) {
+ let slot = 0;
+ return {
+ schema: {
+ stack: slot++,
+ time: slot++,
+ size: slot++,
+ },
+ data: data,
+ };
+}
+
+/**
+ * Deduplicates a profile by deduplicating stacks, frames, and strings.
+ *
+ * This is used to adapt version 2 profiles from the backend to version 3, for
+ * use with older Geckos (like B2G).
+ *
+ * Note that the schemas used by this must be kept in sync with schemas used
+ * by the C++ UniqueStacks class in tools/profiler/ProfileEntry.cpp.
+ *
+ * @param object profile
+ * A profile with version 2.
+ */
+function deflateProfile(profile) {
+ profile.threads = profile.threads.map(thread => {
+ const uniqueStacks = new UniqueStacks();
+ return deflateThread(thread, uniqueStacks);
+ });
+
+ profile.meta.version = 3;
+ return profile;
+}
+
+/**
+ * Given an array of frame objects, deduplicates each frame as well as all
+ * prefixes in the stack. Returns the index of the deduplicated stack.
+ *
+ * @param object frames
+ * Array of frame objects.
+ * @param UniqueStacks uniqueStacks
+ * @return number index
+ */
+function deflateStack(frames, uniqueStacks) {
+ // Deduplicate every prefix in the stack by keeping track of the current
+ // prefix hash.
+ let prefixIndex = null;
+ for (let i = 0; i < frames.length; i++) {
+ const frameIndex = uniqueStacks.getOrAddFrameIndex(frames[i]);
+ prefixIndex = uniqueStacks.getOrAddStackIndex(prefixIndex, frameIndex);
+ }
+ return prefixIndex;
+}
+
+/**
+ * Given an array of sample objects, deduplicate each sample's stack and
+ * convert the samples to a table with a schema. Returns the deflated samples.
+ *
+ * @param object samples
+ * Array of samples
+ * @param UniqueStacks uniqueStacks
+ * @return object
+ */
+function deflateSamples(samples, uniqueStacks) {
+ // Schema:
+ // [stack, time, responsiveness, rss, uss]
+
+ const deflatedSamples = new Array(samples.length);
+ for (let i = 0; i < samples.length; i++) {
+ const sample = samples[i];
+ deflatedSamples[i] = [
+ deflateStack(sample.frames, uniqueStacks),
+ sample.time,
+ sample.responsiveness,
+ sample.rss,
+ sample.uss,
+ ];
+ }
+
+ return samplesWithSchema(deflatedSamples);
+}
+
+/**
+ * Given an array of marker objects, convert the markers to a table with a
+ * schema. Returns the deflated markers.
+ *
+ * If a marker contains a backtrace as its payload, the backtrace stack is
+ * deduplicated in the context of the profile it's in.
+ *
+ * @param object markers
+ * Array of markers
+ * @param UniqueStacks uniqueStacks
+ * @return object
+ */
+function deflateMarkers(markers, uniqueStacks) {
+ // Schema:
+ // [name, time, data]
+
+ const deflatedMarkers = new Array(markers.length);
+ for (let i = 0; i < markers.length; i++) {
+ const marker = markers[i];
+ if (marker.data && marker.data.type === "tracing" && marker.data.stack) {
+ marker.data.stack = deflateThread(marker.data.stack, uniqueStacks);
+ }
+
+ deflatedMarkers[i] = [
+ uniqueStacks.getOrAddStringIndex(marker.name),
+ marker.time,
+ marker.data,
+ ];
+ }
+
+ let slot = 0;
+ return {
+ schema: {
+ name: slot++,
+ time: slot++,
+ data: slot++,
+ },
+ data: deflatedMarkers,
+ };
+}
+
+/**
+ * Deflate a thread.
+ *
+ * @param object thread
+ * The profile thread.
+ * @param UniqueStacks uniqueStacks
+ * @return object
+ */
+function deflateThread(thread, uniqueStacks) {
+ // Some extra threads in a profile come stringified as a full profile (so
+ // it has nested threads itself) so the top level "thread" does not have markers
+ // or samples. We don't use this anyway so just make this safe to deflate.
+ // can be a string rather than an object on import. Bug 1173695
+ if (typeof thread === "string") {
+ thread = JSON.parse(thread);
+ }
+ if (!thread.samples) {
+ thread.samples = [];
+ }
+ if (!thread.markers) {
+ thread.markers = [];
+ }
+
+ return {
+ name: thread.name,
+ tid: thread.tid,
+ samples: deflateSamples(thread.samples, uniqueStacks),
+ markers: deflateMarkers(thread.markers, uniqueStacks),
+ stackTable: uniqueStacks.getStackTableWithSchema(),
+ frameTable: uniqueStacks.getFrameTableWithSchema(),
+ stringTable: uniqueStacks.getStringTable(),
+ };
+}
+
+function stackTableWithSchema(data) {
+ let slot = 0;
+ return {
+ schema: {
+ prefix: slot++,
+ frame: slot++,
+ },
+ data: data,
+ };
+}
+
+function frameTableWithSchema(data) {
+ let slot = 0;
+ return {
+ schema: {
+ location: slot++,
+ implementation: slot++,
+ optimizations: slot++,
+ line: slot++,
+ category: slot++,
+ },
+ data: data,
+ };
+}
+
+function samplesWithSchema(data) {
+ let slot = 0;
+ return {
+ schema: {
+ stack: slot++,
+ time: slot++,
+ responsiveness: slot++,
+ rss: slot++,
+ uss: slot++,
+ },
+ data: data,
+ };
+}
+
+/**
+ * A helper class to deduplicate strings.
+ */
+function UniqueStrings() {
+ this.stringTable = [];
+ this._stringHash = Object.create(null);
+}
+
+UniqueStrings.prototype.getOrAddStringIndex = function(s) {
+ if (!s) {
+ return null;
+ }
+
+ const stringHash = this._stringHash;
+ const stringTable = this.stringTable;
+ let index = stringHash[s];
+ if (index !== undefined) {
+ return index;
+ }
+
+ index = stringTable.length;
+ stringHash[s] = index;
+ stringTable.push(s);
+ return index;
+};
+
+/**
+ * A helper class to deduplicate old-version profiles.
+ *
+ * The main functionality provided is deduplicating frames and stacks.
+ *
+ * For example, given 2 stacks
+ * [A, B, C]
+ * and
+ * [A, B, D]
+ *
+ * There are 4 unique frames: A, B, C, and D.
+ * There are 4 unique prefixes: [A], [A, B], [A, B, C], [A, B, D]
+ *
+ * For the example, the output of using UniqueStacks is:
+ *
+ * Frame table:
+ * [A, B, C, D]
+ *
+ * That is, A has id 0, B has id 1, etc.
+ *
+ * Since stack prefixes are themselves deduplicated (shared), stacks are
+ * represented as a tree, or more concretely, a pair of ids, the prefix and
+ * the leaf.
+ *
+ * Stack table:
+ * [
+ * [null, 0],
+ * [0, 1],
+ * [1, 2],
+ * [1, 3]
+ * ]
+ *
+ * That is, [A] has id 0 and value [null, 0]. This means it has no prefix, and
+ * has the leaf frame 0, which resolves to A in the frame table.
+ *
+ * [A, B] has id 1 and value [0, 1]. This means it has prefix 0, which is [A],
+ * and leaf 1, thus [A, B].
+ *
+ * [A, B, C] has id 2 and value [1, 2]. This means it has prefix 1, which in
+ * turn is [A, B], and leaf 2, thus [A, B, C].
+ *
+ * [A, B, D] has id 3 and value [1, 3]. Note how it shares the prefix 1 with
+ * [A, B, C].
+ */
+function UniqueStacks() {
+ this._frameTable = [];
+ this._stackTable = [];
+ this._frameHash = Object.create(null);
+ this._stackHash = Object.create(null);
+ this._uniqueStrings = new UniqueStrings();
+}
+
+UniqueStacks.prototype.getStackTableWithSchema = function() {
+ return stackTableWithSchema(this._stackTable);
+};
+
+UniqueStacks.prototype.getFrameTableWithSchema = function() {
+ return frameTableWithSchema(this._frameTable);
+};
+
+UniqueStacks.prototype.getStringTable = function() {
+ return this._uniqueStrings.stringTable;
+};
+
+UniqueStacks.prototype.getOrAddFrameIndex = function(frame) {
+ // Schema:
+ // [location, implementation, optimizations, line, category]
+
+ const frameHash = this._frameHash;
+ const frameTable = this._frameTable;
+
+ const locationIndex = this.getOrAddStringIndex(frame.location);
+ const implementationIndex = this.getOrAddStringIndex(frame.implementation);
+
+ // Super dumb.
+ const hash =
+ `${locationIndex} ${implementationIndex || ""} ` +
+ `${frame.line || ""} ${frame.category || ""}`;
+
+ let index = frameHash[hash];
+ if (index !== undefined) {
+ return index;
+ }
+
+ index = frameTable.length;
+ frameHash[hash] = index;
+ frameTable.push([
+ this.getOrAddStringIndex(frame.location),
+ this.getOrAddStringIndex(frame.implementation),
+ // Don't bother with JIT optimization info for deflating old profile data
+ // format to the new format.
+ null,
+ frame.line,
+ frame.category,
+ ]);
+ return index;
+};
+
+UniqueStacks.prototype.getOrAddStackIndex = function(prefixIndex, frameIndex) {
+ // Schema:
+ // [prefix, frame]
+
+ const stackHash = this._stackHash;
+ const stackTable = this._stackTable;
+
+ // Also super dumb.
+ const hash = prefixIndex + " " + frameIndex;
+
+ let index = stackHash[hash];
+ if (index !== undefined) {
+ return index;
+ }
+
+ index = stackTable.length;
+ stackHash[hash] = index;
+ stackTable.push([prefixIndex, frameIndex]);
+ return index;
+};
+
+UniqueStacks.prototype.getOrAddStringIndex = function(s) {
+ return this._uniqueStrings.getOrAddStringIndex(s);
+};
+
+exports.pushAll = pushAll;
+exports.mapRecordingOptions = mapRecordingOptions;
+exports.normalizePerformanceFeatures = normalizePerformanceFeatures;
+exports.filterSamples = filterSamples;
+exports.offsetSampleTimes = offsetSampleTimes;
+exports.offsetMarkerTimes = offsetMarkerTimes;
+exports.offsetAndScaleTimestamps = offsetAndScaleTimestamps;
+exports.getProfileThreadFromAllocations = getProfileThreadFromAllocations;
+exports.deflateProfile = deflateProfile;
+exports.deflateThread = deflateThread;
+exports.UniqueStrings = UniqueStrings;
+exports.UniqueStacks = UniqueStacks;
diff --git a/devtools/shared/performance/xpcshell/.eslintrc.js b/devtools/shared/performance/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..6cb13c0af4
--- /dev/null
+++ b/devtools/shared/performance/xpcshell/.eslintrc.js
@@ -0,0 +1,10 @@
+/* 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";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/performance/xpcshell/head.js b/devtools/shared/performance/xpcshell/head.js
new file mode 100644
index 0000000000..a9ca67f7dd
--- /dev/null
+++ b/devtools/shared/performance/xpcshell/head.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported require */
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
diff --git a/devtools/shared/performance/xpcshell/test_perf-utils-allocations-to-samples.js b/devtools/shared/performance/xpcshell/test_perf-utils-allocations-to-samples.js
new file mode 100644
index 0000000000..e4f545300b
--- /dev/null
+++ b/devtools/shared/performance/xpcshell/test_perf-utils-allocations-to-samples.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if allocations data received from the performance actor is properly
+ * converted to something that follows the same structure as the samples data
+ * received from the profiler.
+ */
+
+add_task(function() {
+ const {
+ getProfileThreadFromAllocations,
+ } = require("devtools/shared/performance/recording-utils");
+ const output = getProfileThreadFromAllocations(TEST_DATA);
+ equal(
+ JSON.stringify(output),
+ JSON.stringify(EXPECTED_OUTPUT),
+ "The output is correct."
+ );
+});
+
+var TEST_DATA = {
+ sites: [0, 0, 1, 2, 3],
+ timestamps: [50, 100, 150, 200, 250],
+ sizes: [0, 0, 100, 200, 300],
+ frames: [
+ null,
+ {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0,
+ },
+ {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1,
+ },
+ {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: null,
+ parent: 2,
+ },
+ ],
+};
+
+var EXPECTED_OUTPUT = {
+ name: "allocations",
+ samples: {
+ schema: {
+ stack: 0,
+ time: 1,
+ size: 2,
+ },
+ data: [
+ [1, 150, 100],
+ [2, 200, 200],
+ [3, 250, 300],
+ ],
+ },
+ stackTable: {
+ schema: {
+ prefix: 0,
+ frame: 1,
+ },
+ data: [
+ null,
+
+ // x (A:1:2)
+ [null, 1],
+
+ // x (A:1:2) > y (B:3:4)
+ [1, 2],
+
+ // x (A:1:2) > y (B:3:4) > C:5:6
+ [2, 3],
+ ],
+ },
+ frameTable: {
+ schema: {
+ location: 0,
+ implementation: 1,
+ optimizations: 2,
+ line: 3,
+ category: 4,
+ },
+ data: [null, [0], [1], [2]],
+ },
+ stringTable: ["x (A:1:2)", "y (B:3:4)", "C:5:6"],
+};
diff --git a/devtools/shared/performance/xpcshell/xpcshell.ini b/devtools/shared/performance/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..87760a972e
--- /dev/null
+++ b/devtools/shared/performance/xpcshell/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_perf-utils-allocations-to-samples.js]