summaryrefslogtreecommitdiffstats
path: root/devtools/shared/performance-new
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/shared/performance-new
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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
3 files changed, 347 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,
+};