diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/shared/performance-new/gecko-profiler-interface.js | 271 | ||||
-rw-r--r-- | devtools/shared/performance-new/moz.build | 13 | ||||
-rw-r--r-- | devtools/shared/performance-new/recording-utils.js | 63 | ||||
-rw-r--r-- | devtools/shared/performance/moz.build | 12 | ||||
-rw-r--r-- | devtools/shared/performance/recording-common.js | 141 | ||||
-rw-r--r-- | devtools/shared/performance/recording-utils.js | 624 | ||||
-rw-r--r-- | devtools/shared/performance/xpcshell/.eslintrc.js | 10 | ||||
-rw-r--r-- | devtools/shared/performance/xpcshell/head.js | 8 | ||||
-rw-r--r-- | devtools/shared/performance/xpcshell/test_perf-utils-allocations-to-samples.js | 97 | ||||
-rw-r--r-- | devtools/shared/performance/xpcshell/xpcshell.ini | 7 |
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] |