/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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"; var { classes: Cc, interfaces: Ci, utils: Cu } = Components; const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); const { AddonWatcher } = Cu.import("resource://gre/modules/AddonWatcher.jsm", {}); const { PerformanceStats } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {}); const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); const { ObjectUtils } = Cu.import("resource://gre/modules/ObjectUtils.jsm", {}); const { Memory } = Cu.import("resource://gre/modules/Memory.jsm"); const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.jsm"); // about:performance observes notifications on this topic. // if a notification is sent, this causes the page to be updated immediately, // regardless of whether the page is on pause. const TEST_DRIVER_TOPIC = "test-about:performance-test-driver"; // about:performance posts notifications on this topic whenever the page // is updated. const UPDATE_COMPLETE_TOPIC = "about:performance-update-complete"; // How often we should add a sample to our buffer. const BUFFER_SAMPLING_RATE_MS = 1000; // The age of the oldest sample to keep. const BUFFER_DURATION_MS = 10000; // How often we should update const UPDATE_INTERVAL_MS = 5000; // The name of the application const BRAND_BUNDLE = Services.strings.createBundle( "chrome://branding/locale/brand.properties"); const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName"); // The maximal number of items to display before showing a "Show All" // button. const MAX_NUMBER_OF_ITEMS_TO_DISPLAY = 3; // If the frequency of alerts is below this value, // we consider that the feature has no impact. const MAX_FREQUENCY_FOR_NO_IMPACT = .05; // If the frequency of alerts is above `MAX_FREQUENCY_FOR_NO_IMPACT` // and below this value, we consider that the feature impacts the // user rarely. const MAX_FREQUENCY_FOR_RARE = .1; // If the frequency of alerts is above `MAX_FREQUENCY_FOR_FREQUENT` // and below this value, we consider that the feature impacts the // user frequently. Anything above is consider permanent. const MAX_FREQUENCY_FOR_FREQUENT = .5; // If the number of high-impact alerts among all alerts is above // this value, we consider that the feature has a major impact // on user experience. const MIN_PROPORTION_FOR_MAJOR_IMPACT = .05; // Otherwise and if the number of medium-impact alerts among all // alerts is above this value, we consider that the feature has // a noticeable impact on user experience. const MIN_PROPORTION_FOR_NOTICEABLE_IMPACT = .1; // The current mode. Either `MODE_GLOBAL` to display a summary of results // since we opened about:performance or `MODE_RECENT` to display the latest // BUFFER_DURATION_MS ms. const MODE_GLOBAL = "global"; const MODE_RECENT = "recent"; let tabFinder = { update: function() { this._map = new Map(); let windows = Services.wm.getEnumerator("navigator:browser"); while (windows.hasMoreElements()) { let win = windows.getNext(); let tabbrowser = win.gBrowser; for (let browser of tabbrowser.browsers) { let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet if (id != null) { this._map.set(id, browser); } } } }, /** * Find the for a window id. * * This is useful e.g. for reloading or closing tabs. * * @return null If the xul:tab could not be found, e.g. if the * windowId is that of a chrome window. * @return {{tabbrowser: , tab: }} The * tabbrowser and tab if the latter could be found. */ get: function(id) { let browser = this._map.get(id); if (!browser) { return null; } let tabbrowser = browser.getTabBrowser(); return {tabbrowser, tab:tabbrowser.getTabForBrowser(browser)}; }, getAny: function(ids) { for (let id of ids) { let result = this.get(id); if (result) { return result; } } return null; } }; /** * Returns a Promise that's resolved after the next turn of the event loop. * * Just returning a resolved Promise would mean that any `then` callbacks * would be called right after the end of the current turn, so `setTimeout` * is used to delay Promise resolution until the next turn. * * In mochi tests, it's possible for this to be called after the * about:performance window has been torn down, which causes `setTimeout` to * throw an NS_ERROR_NOT_INITIALIZED exception. In that case, returning * `undefined` is fine. */ function wait(ms = 0) { try { let resolve; let p = new Promise(resolve_ => { resolve = resolve_ }); setTimeout(resolve, ms); return p; } catch (e) { dump("WARNING: wait aborted because of an invalid Window state in aboutPerformance.js.\n"); return undefined; } } /** * The performance of a webpage or an add-on between two instants. * * Clients should call `promiseInit()` before using the methods of this object. * * @param {PerformanceDiff} The underlying performance data. * @param {"addons"|"webpages"} The kind of delta represented by this object. * @param {Map} ageMap A map containing the oldest known * appearance of each groupId, used to determine how long we have been monitoring * this item. * @param {Map} alertMap A map containing the alerts that each * item has already triggered in the past. */ function Delta(diff, kind, snapshotDate, ageMap, alertMap) { if (kind != "addons" && kind != "webpages") { throw new TypeError(`Unknown kind: ${kind}`); } /** * Either "addons" or "webpages". */ this.kind = kind; /** * The underlying PerformanceDiff. * @type {PerformanceDiff} */ this.diff = diff; /** * A key unique to the item (webpage or add-on), shared by successive * instances of `Delta`. * @type{string} */ this.key = kind + diff.key; // Find the oldest occurrence of this item. let creationDate = snapshotDate; for (let groupId of diff.groupIds) { let date = ageMap.get(groupId); if (date && date <= creationDate) { creationDate = date; } } /** * The timestamp at which the data was measured. */ this.creationDate = creationDate; /** * Number of milliseconds since the start of the measure. */ this.age = snapshotDate - creationDate; /** * A UX-friendly, human-readable name for this item. */ this.readableName = null; /** * A complete name, possibly useful for power users or debugging. */ this.fullName = null; // `true` once initialization is complete. this._initialized = false; // `true` if this item should be displayed this._show = false; /** * All the alerts that this item has caused since about:performance * was opened. */ this.alerts = (alertMap.get(this.key) || []).slice(); switch (this.slowness) { case 0: break; case 1: this.alerts[0] = (this.alerts[0] || 0) + 1; break; case 2: this.alerts[1] = (this.alerts[1] || 0) + 1; break; default: throw new Error(); } } Delta.prototype = { /** * `true` if this item should be displayed, `false` otherwise. */ get show() { this._ensureInitialized(); return this._show; }, /** * Estimate the slowness of this item. * * @return 0 if the item has good performance. * @return 1 if the item has average performance. * @return 2 if the item has poor performance. */ get slowness() { if (Delta.compare(this, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) { return 0; } if (Delta.compare(this, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE) <= 0) { return 1; } return 2; }, _ensureInitialized() { if (!this._initialized) { throw new Error(); } }, /** * Initialize, asynchronously. */ promiseInit: function() { if (this.kind == "webpages") { return this._initWebpage(); } else if (this.kind == "addons") { return this._promiseInitAddon(); } throw new TypeError(); }, _initWebpage: function() { this._initialized = true; let found = tabFinder.getAny(this.diff.windowIds); if (!found || found.tab.linkedBrowser.contentTitle == null) { // Either this is not a real page or the page isn't restored yet. return; } this.readableName = found.tab.linkedBrowser.contentTitle; this.fullName = this.diff.names.join(", "); this._show = true; }, _promiseInitAddon: Task.async(function*() { let found = yield (new Promise(resolve => AddonManager.getAddonByID(this.diff.addonId, a => { if (a) { this.readableName = a.name; resolve(true); } else { resolve(false); } }))); this._initialized = true; // If the add-on manager doesn't know about an add-on, it's // probably not a real add-on. this._show = found; this.fullName = this.diff.addonId; }), toString: function() { return `[Delta] ${this.diff.key} => ${this.readableName}, ${this.fullName}`; } }; Delta.compare = function(a, b) { return ( (a.diff.jank.longestDuration - b.diff.jank.longestDuration) || (a.diff.jank.totalUserTime - b.diff.jank.totalUserTime) || (a.diff.jank.totalSystemTime - b.diff.jank.totalSystemTime) || (a.diff.cpow.totalCPOWTime - b.diff.cpow.totalCPOWTime) || (a.diff.ticks.ticks - b.diff.ticks.ticks) || 0 ); }; Delta.revCompare = function(a, b) { return -Delta.compare(a, b); }; /** * The highest value considered "good performance". */ Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE = { diff: { cpow: { totalCPOWTime: 0, }, jank: { longestDuration: 3, totalUserTime: Number.POSITIVE_INFINITY, totalSystemTime: Number.POSITIVE_INFINITY }, ticks: { ticks: Number.POSITIVE_INFINITY, } } }; /** * The highest value considered "average performance". */ Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE = { diff: { cpow: { totalCPOWTime: Number.POSITIVE_INFINITY, }, jank: { longestDuration: 7, totalUserTime: Number.POSITIVE_INFINITY, totalSystemTime: Number.POSITIVE_INFINITY }, ticks: { ticks: Number.POSITIVE_INFINITY, } } }; /** * Utilities for dealing with state */ var State = { _monitor: PerformanceStats.getMonitor([ "jank", "cpow", "ticks", ]), /** * Indexed by the number of minutes since the snapshot was taken. * * @type {Array} */ _buffer: [], /** * The first snapshot since opening the page. * * @type ApplicationSnapshot */ _oldest: null, /** * The latest snapshot. * * @type ApplicationSnapshot */ _latest: null, /** * The performance alerts for each group. * * This map is cleaned up during each update to avoid leaking references * to groups that have been gc-ed. * * @type{Map} A map in which the keys are provided * by property `key` of instances of `Delta` and the values are arrays * [number of moderate-impact alerts, number of high-impact alerts] */ _alerts: new Map(), /** * The date at which each group was first seen. * * This map is cleaned up during each update to avoid leaking references * to groups that have been gc-ed. * * @type{Map BUFFER_SAMPLING_RATE_MS) { this._latest = yield this._monitor.promiseSnapshot(); this._buffer.push(this._latest); } // If we have too many samples, remove the oldest sample. let oldestInBuffer = this._buffer[0]; if (oldestInBuffer.date + BUFFER_DURATION_MS < this._latest.date) { this._buffer.shift(); } }), /** * @return {Promise} */ promiseDeltaSinceStartOfTime: function() { return this._promiseDeltaSince(this._oldest); }, /** * @return {Promise} */ promiseDeltaSinceStartOfBuffer: function() { return this._promiseDeltaSince(this._buffer[0]); }, /** * @return {Promise} * @resolve {{ * addons: Array, * webpages: Array, * deltas: Set, * duration: number of milliseconds * }} */ _promiseDeltaSince: Task.async(function*(oldest) { let current = this._latest; if (!oldest) { throw new TypeError(); } if (!current) { throw new TypeError(); } tabFinder.update(); // We rebuild the maps during each iteration to make sure that // we do not maintain references to groups that has been removed // (e.g. pages that have been closed). let oldFirstSeen = this._firstSeen; let cleanedUpFirstSeen = new Map(); let oldAlerts = this._alerts; let cleanedUpAlerts = new Map(); let result = { addons: [], webpages: [], deltas: new Set(), duration: current.date - oldest.date }; for (let kind of ["webpages", "addons"]) { for (let [key, value] of current[kind]) { let item = ObjectUtils.strict(new Delta(value.subtract(oldest[kind].get(key)), kind, current.date, oldFirstSeen, oldAlerts)); yield item.promiseInit(); if (!item.show) { continue; } result[kind].push(item); result.deltas.add(item.key); for (let groupId of item.diff.groupIds) { cleanedUpFirstSeen.set(groupId, item.creationDate); } cleanedUpAlerts.set(item.key, item.alerts); } } this._firstSeen = cleanedUpFirstSeen; this._alerts = cleanedUpAlerts; return result; }), }; var View = { /** * A cache for all the per-item DOM elements that are reused across refreshes. * * Reusing the same elements means that elements that were hidden (respectively * visible) in an iteration remain hidden (resp visible) in the next iteration. */ DOMCache: { _map: new Map(), /** * @param {string} deltaKey The key for the item that we are displaying. * @return {null} If the `deltaKey` doesn't have a component cached yet. * Otherwise, the value stored with `set`. */ get: function(deltaKey) { return this._map.get(deltaKey); }, set: function(deltaKey, value) { this._map.set(deltaKey, value); }, /** * Remove all the elements whose key does not appear in `set`. * * @param {Set} set a set of deltaKey. */ trimTo: function(set) { let remove = []; for (let key of this._map.keys()) { if (!set.has(key)) { remove.push(key); } } for (let key of remove) { this._map.delete(key); } } }, /** * Display the items in a category. * * @param {Array} subset The items to display. They will * be displayed in the order of `subset`. * @param {string} id The id of the DOM element that will contain the items. * @param {string} nature The nature of the subset. One of "addons", "webpages" or "system". * @param {string} currentMode The current display mode. One of MODE_GLOBAL or MODE_RECENT. */ updateCategory: function(subset, id, nature, currentMode) { subset = subset.slice().sort(Delta.revCompare); let watcherAlerts = null; if (nature == "addons") { watcherAlerts = AddonWatcher.alerts; } // Grab everything from the DOM before cleaning up this._setupStructure(id); // An array of `cachedElements` that need to be added let toAdd = []; for (let delta of subset) { if (!(delta instanceof Delta)) { throw new TypeError(); } let cachedElements = this._grabOrCreateElements(delta, nature); toAdd.push(cachedElements); cachedElements.eltTitle.textContent = delta.readableName; cachedElements.eltName.textContent = `Full name: ${delta.fullName}.`; cachedElements.eltLoaded.textContent = `Measure start: ${Math.round(delta.age/1000)} seconds ago.` let processes = delta.diff.processes.map(proc => `${proc.processId} (${proc.isChildProcess?"child":"parent"})`); cachedElements.eltProcess.textContent = `Processes: ${processes.join(", ")}`; let jankSuffix = ""; if (watcherAlerts) { let deltaAlerts = watcherAlerts.get(delta.diff.addonId); if (deltaAlerts) { if (deltaAlerts.occurrences) { jankSuffix = ` (${deltaAlerts.occurrences} alerts)`; } } } let eltImpact = cachedElements.eltImpact; if (currentMode == MODE_RECENT) { cachedElements.eltRoot.setAttribute("impact", delta.diff.jank.longestDuration + 1); if (Delta.compare(delta, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) { eltImpact.textContent = ` currently performs well.`; } else if (Delta.compare(delta, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE)) { eltImpact.textContent = ` may currently be slowing down ${BRAND_NAME}.`; } else { eltImpact.textContent = ` is currently considerably slowing down ${BRAND_NAME}.`; } cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.diff.jank.longestDuration + 1}/${delta.diff.jank.durations.length}${jankSuffix}.`; cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}%.`; cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}%.`; cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}%.`; } else { if (delta.alerts.length == 0) { eltImpact.textContent = " has performed well so far."; cachedElements.eltFPS.textContent = `Impact on framerate: no impact.`; cachedElements.eltRoot.setAttribute("impact", 0); } else { let impact = 0; let sum = /* medium impact */ delta.alerts[0] + /* high impact */ delta.alerts[1]; let frequency = sum * 1000 / delta.diff.deltaT; let describeFrequency; if (frequency <= MAX_FREQUENCY_FOR_NO_IMPACT) { describeFrequency = `has no impact on the performance of ${BRAND_NAME}.` } else { let describeImpact; if (frequency <= MAX_FREQUENCY_FOR_RARE) { describeFrequency = `rarely slows down ${BRAND_NAME}.`; impact += 1; } else if (frequency <= MAX_FREQUENCY_FOR_FREQUENT) { describeFrequency = `has slown down ${BRAND_NAME} frequently.`; impact += 2.5; } else { describeFrequency = `seems to have slown down ${BRAND_NAME} very often.`; impact += 5; } // At this stage, `sum != 0` if (delta.alerts[1] / sum > MIN_PROPORTION_FOR_MAJOR_IMPACT) { describeImpact = "When this happens, the slowdown is generally important." impact *= 2; } else { describeImpact = "When this happens, the slowdown is generally noticeable." } eltImpact.textContent = ` ${describeFrequency} ${describeImpact}`; cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1] || 0} high-impacts, ${delta.alerts[0] || 0} medium-impact${jankSuffix}.`; } cachedElements.eltRoot.setAttribute("impact", Math.round(impact)); } cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalUserTime}ms).`; cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalSystemTime}ms).`; cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}% (total ${delta.diff.cpow.totalCPOWTime}ms).`; } } this._insertElements(toAdd, id); }, _insertElements: function(elements, id) { let eltContainer = document.getElementById(id); eltContainer.classList.remove("measuring"); eltContainer.eltVisibleContent.innerHTML = ""; eltContainer.eltHiddenContent.innerHTML = ""; eltContainer.appendChild(eltContainer.eltShowMore); for (let i = 0; i < elements.length && i < MAX_NUMBER_OF_ITEMS_TO_DISPLAY; ++i) { let cachedElements = elements[i]; eltContainer.eltVisibleContent.appendChild(cachedElements.eltRoot); } for (let i = MAX_NUMBER_OF_ITEMS_TO_DISPLAY; i < elements.length; ++i) { let cachedElements = elements[i]; eltContainer.eltHiddenContent.appendChild(cachedElements.eltRoot); } if (elements.length <= MAX_NUMBER_OF_ITEMS_TO_DISPLAY) { eltContainer.eltShowMore.classList.add("hidden"); } else { eltContainer.eltShowMore.classList.remove("hidden"); } if (elements.length == 0) { eltContainer.textContent = "Nothing"; } }, _setupStructure: function(id) { let eltContainer = document.getElementById(id); if (!eltContainer.eltVisibleContent) { eltContainer.eltVisibleContent = document.createElement("ul"); eltContainer.eltVisibleContent.classList.add("visible_items"); eltContainer.appendChild(eltContainer.eltVisibleContent); } if (!eltContainer.eltHiddenContent) { eltContainer.eltHiddenContent = document.createElement("ul"); eltContainer.eltHiddenContent.classList.add("hidden"); eltContainer.eltHiddenContent.classList.add("hidden_additional_items"); eltContainer.appendChild(eltContainer.eltHiddenContent); } if (!eltContainer.eltShowMore) { eltContainer.eltShowMore = document.createElement("button"); eltContainer.eltShowMore.textContent = "Show all"; eltContainer.eltShowMore.classList.add("show_all_items"); eltContainer.appendChild(eltContainer.eltShowMore); eltContainer.eltShowMore.addEventListener("click", function() { if (eltContainer.eltHiddenContent.classList.contains("hidden")) { eltContainer.eltHiddenContent.classList.remove("hidden"); eltContainer.eltShowMore.textContent = "Hide"; } else { eltContainer.eltHiddenContent.classList.add("hidden"); eltContainer.eltShowMore.textContent = "Show all"; } }); } return eltContainer; }, _grabOrCreateElements: function(delta, nature) { let cachedElements = this.DOMCache.get(delta.key); if (cachedElements) { if (cachedElements.eltRoot.parentElement) { cachedElements.eltRoot.parentElement.removeChild(cachedElements.eltRoot); } } else { this.DOMCache.set(delta.key, cachedElements = {}); let eltDelta = document.createElement("li"); eltDelta.classList.add("delta"); cachedElements.eltRoot = eltDelta; let eltSpan = document.createElement("span"); eltDelta.appendChild(eltSpan); let eltSummary = document.createElement("span"); eltSummary.classList.add("summary"); eltSpan.appendChild(eltSummary); let eltTitle = document.createElement("span"); eltTitle.classList.add("title"); eltSummary.appendChild(eltTitle); cachedElements.eltTitle = eltTitle; let eltImpact = document.createElement("span"); eltImpact.classList.add("impact"); eltSummary.appendChild(eltImpact); cachedElements.eltImpact = eltImpact; let eltShowMore = document.createElement("a"); eltShowMore.classList.add("more"); eltSpan.appendChild(eltShowMore); eltShowMore.textContent = "more"; eltShowMore.href = ""; eltShowMore.addEventListener("click", () => { if (eltDetails.classList.contains("hidden")) { eltDetails.classList.remove("hidden"); eltShowMore.textContent = "less"; } else { eltDetails.classList.add("hidden"); eltShowMore.textContent = "more"; } }); // Add buttons if (nature == "addons") { eltSpan.appendChild(document.createElement("br")); let eltDisable = document.createElement("button"); eltDisable.textContent = "Disable"; eltSpan.appendChild(eltDisable); let eltUninstall = document.createElement("button"); eltUninstall.textContent = "Uninstall"; eltSpan.appendChild(eltUninstall); let eltRestart = document.createElement("button"); eltRestart.textContent = `Restart ${BRAND_NAME} to apply your changes.` eltRestart.classList.add("hidden"); eltSpan.appendChild(eltRestart); eltRestart.addEventListener("click", () => { Services.startup.quit(Services.startup.eForceQuit | Services.startup.eRestart); }); AddonManager.getAddonByID(delta.diff.addonId, addon => { eltDisable.addEventListener("click", () => { addon.userDisabled = true; if (addon.pendingOperations == addon.PENDING_NONE) { // Restartless add-on is now disabled. return; } eltDisable.classList.add("hidden"); eltUninstall.classList.add("hidden"); eltRestart.classList.remove("hidden"); }); eltUninstall.addEventListener("click", () => { addon.uninstall(); if (addon.pendingOperations == addon.PENDING_NONE) { // Restartless add-on is now disabled. return; } eltDisable.classList.add("hidden"); eltUninstall.classList.add("hidden"); eltRestart.classList.remove("hidden"); }); }); } else if (nature == "webpages") { eltSpan.appendChild(document.createElement("br")); let eltCloseTab = document.createElement("button"); eltCloseTab.textContent = "Close tab"; eltSpan.appendChild(eltCloseTab); let windowIds = delta.diff.windowIds; eltCloseTab.addEventListener("click", () => { let found = tabFinder.getAny(windowIds); if (!found) { // Cannot find the tab. Maybe it is closed already? return; } let {tabbrowser, tab} = found; tabbrowser.removeTab(tab); }); let eltReloadTab = document.createElement("button"); eltReloadTab.textContent = "Reload tab"; eltSpan.appendChild(eltReloadTab); eltReloadTab.addEventListener("click", () => { let found = tabFinder.getAny(windowIds); if (!found) { // Cannot find the tab. Maybe it is closed already? return; } let {tabbrowser, tab} = found; tabbrowser.reloadTab(tab); }); } // Prepare details let eltDetails = document.createElement("ul"); eltDetails.classList.add("details"); eltDetails.classList.add("hidden"); eltSpan.appendChild(eltDetails); for (let [name, className] of [ ["eltName", "name"], ["eltFPS", "fps"], ["eltCPU", "cpu"], ["eltSystem", "system"], ["eltCPOW", "cpow"], ["eltLoaded", "loaded"], ["eltProcess", "process"], ]) { let elt = document.createElement("li"); elt.classList.add(className); eltDetails.appendChild(elt); cachedElements[name] = elt; } } return cachedElements; }, }; var Control = { init: function() { this._initAutorefresh(); this._initDisplayMode(); }, update: Task.async(function*() { let mode = this._displayMode; if (this._autoRefreshInterval || !State._buffer[0]) { // Update the state only if we are not on pause. yield State.update(); } yield wait(0); let state = yield (mode == MODE_GLOBAL? State.promiseDeltaSinceStartOfTime(): State.promiseDeltaSinceStartOfBuffer()); for (let category of ["webpages", "addons"]) { yield wait(0); yield View.updateCategory(state[category], category, category, mode); } yield wait(0); // Make sure that we do not keep obsolete stuff around. View.DOMCache.trimTo(state.deltas); yield wait(0); // Inform watchers Services.obs.notifyObservers(null, UPDATE_COMPLETE_TOPIC, mode); }), _setOptions: function(options) { dump(`about:performance _setOptions ${JSON.stringify(options)}\n`); let eltRefresh = document.getElementById("check-autorefresh"); if ((options.autoRefresh > 0) != eltRefresh.checked) { eltRefresh.click(); } let eltCheckRecent = document.getElementById("check-display-recent"); if (!!options.displayRecent != eltCheckRecent.checked) { eltCheckRecent.click(); } }, _initAutorefresh: function() { let onRefreshChange = (shouldUpdateNow = false) => { if (eltRefresh.checked == !!this._autoRefreshInterval) { // Nothing to change. return; } if (eltRefresh.checked) { this._autoRefreshInterval = window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS); if (shouldUpdateNow) { Control.update(); } } else { window.clearInterval(this._autoRefreshInterval); this._autoRefreshInterval = null; } } let eltRefresh = document.getElementById("check-autorefresh"); eltRefresh.addEventListener("change", () => onRefreshChange(true)); onRefreshChange(false); }, _autoRefreshInterval: null, _initDisplayMode: function() { let onModeChange = (shouldUpdateNow) => { if (eltCheckRecent.checked) { this._displayMode = MODE_RECENT; } else { this._displayMode = MODE_GLOBAL; } if (shouldUpdateNow) { Control.update(); } }; let eltCheckRecent = document.getElementById("check-display-recent"); let eltLabelRecent = document.getElementById("label-display-recent"); eltCheckRecent.addEventListener("click", () => onModeChange(true)); eltLabelRecent.textContent = `Display only the latest ${Math.round(BUFFER_DURATION_MS/1000)}s`; onModeChange(false); }, // The display mode. One of `MODE_GLOBAL` or `MODE_RECENT`. _displayMode: MODE_GLOBAL, }; /** * This functionality gets memory related information of sub-processes and * updates the performance table regularly. * If the page goes hidden, it also handles visibility change by not * querying the content processes unnecessarily. */ var SubprocessMonitor = { _timeout: null, /** * Init will start the process of updating the table if the page is not hidden, * and set up an event listener for handling visibility changes. */ init: function() { if (!document.hidden) { SubprocessMonitor.updateTable(); } document.addEventListener("visibilitychange", SubprocessMonitor.handleVisibilityChange); }, /** * This function updates the table after an interval if the page is visible * and clears the interval otherwise. */ handleVisibilityChange: function() { if (!document.hidden) { SubprocessMonitor.queueUpdate(); } else { clearTimeout(this._timeout); this._timeout = null; } }, /** * This function queues a timer to request the next summary using updateTable * after some delay. */ queueUpdate: function() { this._timeout = setTimeout(() => this.updateTable(), UPDATE_INTERVAL_MS); }, /** * This is a helper function for updateTable, which updates a particular row. * @param { node} row The row to be updated. * @param {object} summaries The object with the updated RSS and USS values. * @param {string} pid The pid represented by the row for which we update. */ updateRow: function(row, summaries, pid) { row.cells[0].textContent = pid; let RSSval = DownloadUtils.convertByteUnits(summaries[pid].rss); row.cells[1].textContent = RSSval.join(" "); let USSval = DownloadUtils.convertByteUnits(summaries[pid].uss); row.cells[2].textContent = USSval.join(" "); }, /** * This function adds a row to the subprocess-performance table for every new pid * and populates and regularly updates it with RSS/USS measurements. */ updateTable: function() { if (!document.hidden) { Memory.summary().then((summaries) => { if (!(Object.keys(summaries).length)) { // The summaries list was empty, which means we timed out getting // the memory reports. We'll try again later. SubprocessMonitor.queueUpdate(); return; } let resultTable = document.getElementById("subprocess-reports"); let recycle = []; // We first iterate the table to check if summaries exist for rowPids, // if yes, update them and delete the pid's summary or else hide the row // for recycling it. Start at row 1 instead of 0 (to skip the header row). for (let i = 1, row; row = resultTable.rows[i]; i++) { let rowPid = row.dataset.pid; let summary = summaries[rowPid]; if (summary) { // Now we update the values in the row, which is hardcoded for now, // but we might want to make this more adaptable in the future. SubprocessMonitor.updateRow(row, summaries, rowPid); delete summaries[rowPid]; } else { // Take this unnecessary row, hide it and stash it for potential re-use. row.hidden = true; recycle.push(row); } } // For the remaining pids in summaries, we choose from the recyclable // (hidden) nodes, and if they get exhausted, append a row to the table. for (let pid in summaries) { let row = recycle.pop(); if (row) { row.hidden = false; } else { // We create a new row here, and set it to row row = document.createElement("tr"); // Insert cell for pid row.insertCell(); // Insert a cell for USS. row.insertCell(); // Insert another cell for RSS. row.insertCell(); } row.dataset.pid = pid; // Update the row and put it at the bottom SubprocessMonitor.updateRow(row, summaries, pid); resultTable.appendChild(row); } }); SubprocessMonitor.queueUpdate(); } }, }; var go = Task.async(function*() { SubprocessMonitor.init(); Control.init(); // Setup a hook to allow tests to configure and control this page let testUpdate = function(subject, topic, value) { let options = JSON.parse(value); Control._setOptions(options); Control.update(); }; Services.obs.addObserver(testUpdate, TEST_DRIVER_TOPIC, false); window.addEventListener("unload", () => Services.obs.removeObserver(testUpdate, TEST_DRIVER_TOPIC)); yield Control.update(); yield wait(BUFFER_SAMPLING_RATE_MS * 1.1); yield Control.update(); });