summaryrefslogtreecommitdiffstats
path: root/devtools/shared/performance-new/gecko-profiler-interface.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/shared/performance-new/gecko-profiler-interface.js271
1 files changed, 271 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;