summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/popup/panel.jsm.js
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/client/performance-new/popup/panel.jsm.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance-new/popup/panel.jsm.js')
-rw-r--r--devtools/client/performance-new/popup/panel.jsm.js386
1 files changed, 386 insertions, 0 deletions
diff --git a/devtools/client/performance-new/popup/panel.jsm.js b/devtools/client/performance-new/popup/panel.jsm.js
new file mode 100644
index 0000000000..33e2e6fb4f
--- /dev/null
+++ b/devtools/client/performance-new/popup/panel.jsm.js
@@ -0,0 +1,386 @@
+/* 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 controls the logic of the profiler popup view.
+ */
+
+/**
+ * @typedef {ReturnType<typeof selectElementsInPanelview>} Elements
+ * @typedef {ReturnType<typeof createViewControllers>} ViewController
+ */
+
+/**
+ * @typedef {Object} State - The mutable state of the popup.
+ * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden.
+ * @property {boolean} isInfoCollapsed
+ */
+
+const { createLazyLoaders } = ChromeUtils.import(
+ "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
+
+const lazy = createLazyLoaders({
+ Services: () => ChromeUtils.import("resource://gre/modules/Services.jsm"),
+ PanelMultiView: () =>
+ ChromeUtils.import("resource:///modules/PanelMultiView.jsm"),
+ Background: () =>
+ ChromeUtils.import(
+ "resource://devtools/client/performance-new/popup/background.jsm.js"
+ ),
+});
+
+/**
+ * This function collects all of the selection of the elements inside of the panel.
+ *
+ * @param {XULElement} panelview
+ */
+function selectElementsInPanelview(panelview) {
+ const document = panelview.ownerDocument;
+ /**
+ * Get an element or throw an error if it's not found. This is more friendly
+ * for TypeScript.
+ *
+ * @param {string} id
+ * @return {HTMLElement}
+ */
+ function getElementById(id) {
+ /** @type {HTMLElement | null} */
+ // @ts-ignore - Bug 1674368
+ const { PanelMultiView } = lazy.PanelMultiView();
+ const element = PanelMultiView.getViewNode(document, id);
+ if (!element) {
+ throw new Error(`Could not find the element from the ID "${id}"`);
+ }
+ return element;
+ }
+
+ return {
+ document,
+ panelview,
+ window: /** @type {ChromeWindow} */ (document.defaultView),
+ inactive: getElementById("PanelUI-profiler-inactive"),
+ active: getElementById("PanelUI-profiler-active"),
+ locked: getElementById("PanelUI-profiler-locked"),
+ presetDescription: getElementById("PanelUI-profiler-content-description"),
+ presetCustom: getElementById("PanelUI-profiler-content-custom"),
+ presetsCustomButton: getElementById(
+ "PanelUI-profiler-content-custom-button"
+ ),
+ presetsMenuList: /** @type {MenuListElement} */ (getElementById(
+ "PanelUI-profiler-presets"
+ )),
+ header: getElementById("PanelUI-profiler-header"),
+ info: getElementById("PanelUI-profiler-info"),
+ menupopup: getElementById("PanelUI-profiler-presets-menupopup"),
+ infoButton: getElementById("PanelUI-profiler-info-button"),
+ learnMore: getElementById("PanelUI-profiler-learn-more"),
+ startRecording: getElementById("PanelUI-profiler-startRecording"),
+ stopAndDiscard: getElementById("PanelUI-profiler-stopAndDiscard"),
+ stopAndCapture: getElementById("PanelUI-profiler-stopAndCapture"),
+ settingsSection: getElementById("PanelUI-profiler-content-settings"),
+ contentRecording: getElementById("PanelUI-profiler-content-recording"),
+ };
+}
+
+/**
+ * This function returns an interface that can be used to control the view of the
+ * panel based on the current mutable State.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ */
+function createViewControllers(state, elements) {
+ return {
+ updateInfoCollapse() {
+ const { header, info, infoButton, panelview } = elements;
+ header.setAttribute(
+ "isinfocollapsed",
+ state.isInfoCollapsed ? "true" : "false"
+ );
+ // @ts-ignore - Bug 1674368
+ panelview
+ .closest("panel")
+ .setAttribute(
+ "isinfoexpanded",
+ state.isInfoCollapsed ? "false" : "true"
+ );
+ // @ts-ignore - Bug 1674368
+ infoButton.checked = !state.isInfoCollapsed;
+
+ if (state.isInfoCollapsed) {
+ const { height } = info.getBoundingClientRect();
+ info.style.marginBlockEnd = `-${height}px`;
+ } else {
+ info.style.marginBlockEnd = "0";
+ }
+ },
+
+ updatePresets() {
+ const { Services } = lazy.Services();
+ const { presets, getRecordingPreferences } = lazy.Background();
+ const { presetName } = getRecordingPreferences(
+ "aboutprofiling",
+ Services.profiler.GetFeatures()
+ );
+ const preset = presets[presetName];
+ if (preset) {
+ elements.presetDescription.style.display = "block";
+ elements.presetCustom.style.display = "none";
+ elements.presetDescription.textContent = preset.description;
+ elements.presetsMenuList.value = presetName;
+ // This works around XULElement height issues.
+ const { height } = elements.presetDescription.getBoundingClientRect();
+ elements.presetDescription.style.height = `${height}px`;
+ } else {
+ elements.presetDescription.style.display = "none";
+ elements.presetCustom.style.display = "block";
+ elements.presetsMenuList.value = "custom";
+ }
+ const { PanelMultiView } = lazy.PanelMultiView();
+ // Update the description height sizing.
+ PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
+ },
+
+ updateProfilerState() {
+ const { Services } = lazy.Services();
+ /**
+ * Convert two boolean values into a "profilerState" enum.
+ *
+ * @type {"active" | "inactive" | "locked"}
+ */
+ let profilerState = Services.profiler.IsActive() ? "active" : "inactive";
+ if (!Services.profiler.CanProfile()) {
+ // In private browsing mode, the profiler is locked.
+ profilerState = "locked";
+ }
+
+ switch (profilerState) {
+ case "active":
+ elements.inactive.setAttribute("hidden", "true");
+ elements.active.setAttribute("hidden", "false");
+ elements.settingsSection.setAttribute("hidden", "true");
+ elements.contentRecording.setAttribute("hidden", "false");
+ elements.locked.setAttribute("hidden", "true");
+ break;
+ case "inactive":
+ elements.inactive.setAttribute("hidden", "false");
+ elements.active.setAttribute("hidden", "true");
+ elements.settingsSection.setAttribute("hidden", "false");
+ elements.contentRecording.setAttribute("hidden", "true");
+ elements.locked.setAttribute("hidden", "true");
+ break;
+ case "locked": {
+ elements.inactive.setAttribute("hidden", "true");
+ elements.active.setAttribute("hidden", "true");
+ elements.settingsSection.setAttribute("hidden", "true");
+ elements.contentRecording.setAttribute("hidden", "true");
+ elements.locked.setAttribute("hidden", "false");
+ // This works around XULElement height issues.
+ const { height } = elements.locked.getBoundingClientRect();
+ elements.locked.style.height = `${height}px`;
+ break;
+ }
+ default:
+ throw new Error("Unhandled profiler state.");
+ }
+ },
+
+ createPresetsList() {
+ // Check the DOM if the presets were built or not. We can't cache this value
+ // in the `State` object, as the `State` object will be removed if the
+ // button is removed from the toolbar, but the DOM changes will still persist.
+ if (elements.menupopup.getAttribute("presetsbuilt") === "true") {
+ // The presets were already built.
+ return;
+ }
+ const { Services } = lazy.Services();
+ const { presets } = lazy.Background();
+ const currentPreset = Services.prefs.getCharPref(
+ "devtools.performance.recording.preset"
+ );
+
+ const menuitems = Object.entries(presets).map(([id, preset]) => {
+ const menuitem = elements.document.createXULElement("menuitem");
+ menuitem.setAttribute("label", preset.label);
+ menuitem.setAttribute("value", id);
+ if (id === currentPreset) {
+ elements.presetsMenuList.setAttribute("value", id);
+ }
+ return menuitem;
+ });
+
+ elements.menupopup.prepend(...menuitems);
+ elements.menupopup.setAttribute("presetsbuilt", "true");
+ },
+
+ hidePopup() {
+ const panel = elements.panelview.closest("panel");
+ if (!panel) {
+ throw new Error("Could not find the panel from the panelview.");
+ }
+ panel.removeAttribute("isinfoexpanded");
+ /** @type {any} */ (panel).hidePopup();
+ },
+ };
+}
+
+/**
+ * Perform all of the business logic to present the popup view once it is open.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function initializePopup(state, elements, view) {
+ view.createPresetsList();
+
+ state.cleanup.push(() => {
+ // The UI should be collapsed by default for the next time the popup
+ // is open.
+ state.isInfoCollapsed = true;
+ view.updateInfoCollapse();
+ });
+
+ // Turn off all animations while initializing the popup.
+ elements.header.setAttribute("animationready", "false");
+
+ elements.window.requestAnimationFrame(() => {
+ // Allow the elements to layout once, the updateInfoCollapse implementation measures
+ // the size of the container. It needs to wait a second before the bounding box
+ // returns an actual size.
+ view.updateInfoCollapse();
+ view.updateProfilerState();
+ view.updatePresets();
+
+ // XUL <description> elements don't vertically size correctly, this is
+ // the workaround for it.
+ const { PanelMultiView } = lazy.PanelMultiView();
+ PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
+
+ // Now wait for another rAF, and turn the animations back on.
+ elements.window.requestAnimationFrame(() => {
+ elements.header.setAttribute("animationready", "true");
+ });
+ });
+}
+
+/**
+ * This function is in charge of settings all of the events handlers for the view.
+ * The handlers must also add themselves to the `state.cleanup` for them to be
+ * properly cleaned up once the view is destroyed.
+ *
+ * @param {State} state
+ * @param {Elements} elements
+ * @param {ViewController} view
+ */
+function addPopupEventHandlers(state, elements, view) {
+ const {
+ changePreset,
+ startProfiler,
+ stopProfiler,
+ captureProfile,
+ } = lazy.Background();
+
+ /**
+ * Adds a handler that automatically is removed once the panel is hidden.
+ *
+ * @param {HTMLElement} element
+ * @param {string} type
+ * @param {(event: Event) => void} handler
+ */
+ function addHandler(element, type, handler) {
+ element.addEventListener(type, handler);
+ state.cleanup.push(() => {
+ element.removeEventListener(type, handler);
+ });
+ }
+
+ addHandler(elements.infoButton, "click", event => {
+ // Any button command event in the popup will cause it to close. Prevent this
+ // from happening on click.
+ event.preventDefault();
+
+ state.isInfoCollapsed = !state.isInfoCollapsed;
+ view.updateInfoCollapse();
+ });
+
+ addHandler(elements.startRecording, "click", () => {
+ startProfiler("aboutprofiling");
+ });
+
+ addHandler(elements.stopAndDiscard, "click", () => {
+ stopProfiler();
+ });
+
+ addHandler(elements.stopAndCapture, "click", () => {
+ captureProfile("aboutprofiling");
+ view.hidePopup();
+ });
+
+ addHandler(elements.learnMore, "click", () => {
+ elements.window.openWebLinkIn("https://profiler.firefox.com/docs/", "tab");
+ view.hidePopup();
+ });
+
+ addHandler(elements.presetsMenuList, "command", () => {
+ changePreset(
+ "aboutprofiling",
+ elements.presetsMenuList.value,
+ Services.profiler.GetFeatures()
+ );
+ view.updatePresets();
+ });
+
+ addHandler(elements.presetsMenuList, "popuphidden", event => {
+ // Changing a preset makes the popup autohide, this handler stops the
+ // propagation of that event, so that only the menulist's popup closes,
+ // and not the rest of the popup.
+ event.stopPropagation();
+ });
+
+ addHandler(elements.presetsMenuList, "click", event => {
+ // Clicking on a preset makes the popup autohide, this preventDefault stops
+ // the CustomizableUI from closing the popup.
+ event.preventDefault();
+ });
+
+ addHandler(elements.presetsCustomButton, "click", () => {
+ elements.window.openTrustedLinkIn("about:profiling", "tab");
+ view.hidePopup();
+ });
+
+ // Update the view when the profiler starts/stops.
+ const { Services } = lazy.Services();
+
+ // These are all events that can affect the current state of the profiler.
+ const events = [
+ "profiler-started",
+ "profiler-stopped",
+ "chrome-document-global-created", // This is potentially a private browser.
+ "last-pb-context-exited",
+ ];
+ for (const event of events) {
+ Services.obs.addObserver(view.updateProfilerState, event);
+ state.cleanup.push(() => {
+ Services.obs.removeObserver(view.updateProfilerState, event);
+ });
+ }
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */ (this).module = {};
+
+module.exports = {
+ selectElementsInPanelview,
+ createViewControllers,
+ addPopupEventHandlers,
+ initializePopup,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);