fune/toolkit/components/aboutperformance/content/aboutPerformance.js

1088 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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";
const { AddonManager } = ChromeUtils.import(
"resource://gre/modules/AddonManager.jsm"
);
const { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
// Time in ms before we start changing the sort order again after receiving a
// mousemove event.
const TIME_BEFORE_SORTING_AGAIN = 5000;
// 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 = 2000;
// The name of the application
const BRAND_BUNDLE = Services.strings.createBundle(
"chrome://branding/locale/brand.properties"
);
const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
function extensionCountersEnabled() {
return Services.prefs.getBoolPref(
"extensions.webextensions.enablePerformanceCounters",
false
);
}
// The ids of system add-ons, so that we can hide them when the
// toolkit.aboutPerformance.showInternals pref is false.
// The API to access addons is async, so we cache the list during init.
// The list is unlikely to change while the about:performance
// tab is open, so not updating seems fine.
var gSystemAddonIds = new Set();
let tabFinder = {
update() {
this._map = new Map();
for (let win of Services.wm.getEnumerator("navigator:browser")) {
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);
}
}
if (tabbrowser.preloadedBrowser) {
let browser = tabbrowser.preloadedBrowser;
if (browser.outerWindowID) {
this._map.set(browser.outerWindowID, browser);
}
}
}
},
/**
* Find the <xul:tab> 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: <xul:tabbrowser>, tab: <xul.tab>}} The
* tabbrowser and tab if the latter could be found.
*/
get(id) {
let browser = this._map.get(id);
if (!browser) {
return null;
}
let tabbrowser = browser.getTabBrowser();
if (!tabbrowser) {
return {
tabbrowser: null,
tab: {
getAttribute() {
return "";
},
linkedBrowser: browser,
},
};
}
return { tabbrowser, tab: tabbrowser.getTabForBrowser(browser) };
},
getAny(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;
}
}
/**
* Utilities for dealing with state
*/
var State = {
/**
* Indexed by the number of minutes since the snapshot was taken.
*
* @type {Array<ApplicationSnapshot>}
*/
_buffer: [],
/**
* The latest snapshot.
*
* @type ApplicationSnapshot
*/
_latest: null,
async _promiseSnapshot() {
let addons = WebExtensionPolicy.getActiveExtensions();
let addonHosts = new Map();
for (let addon of addons) {
addonHosts.set(addon.mozExtensionHostname, addon.id);
}
let counters = await ChromeUtils.requestPerformanceMetrics();
let tabs = {};
for (let counter of counters) {
let {
items,
host,
pid,
counterId,
windowId,
duration,
isWorker,
memoryInfo,
isTopLevel,
} = counter;
// If a worker has a windowId of 0 or max uint64, attach it to the
// browser UI (doc group with id 1).
if (isWorker && (windowId == 18446744073709552000 || !windowId)) {
windowId = 1;
}
let dispatchCount = 0;
for (let { count } of items) {
dispatchCount += count;
}
let memory = 0;
for (let field in memoryInfo) {
if (field == "media") {
for (let mediaField of ["audioSize", "videoSize", "resourcesSize"]) {
memory += memoryInfo.media[mediaField];
}
continue;
}
memory += memoryInfo[field];
}
let tab;
let id = windowId;
if (addonHosts.has(host)) {
id = addonHosts.get(host);
}
if (id in tabs) {
tab = tabs[id];
} else {
tab = {
windowId,
host,
dispatchCount: 0,
duration: 0,
memory: 0,
children: [],
};
tabs[id] = tab;
}
tab.dispatchCount += dispatchCount;
tab.duration += duration;
tab.memory += memory;
if (!isTopLevel || isWorker) {
tab.children.push({
host,
isWorker,
dispatchCount,
duration,
memory,
counterId: pid + ":" + counterId,
});
}
}
if (extensionCountersEnabled()) {
let extCounters = await ExtensionParent.ParentAPIManager.retrievePerformanceCounters();
for (let [id, apiMap] of extCounters) {
let dispatchCount = 0,
duration = 0;
for (let [, counter] of apiMap) {
dispatchCount += counter.calls;
duration += counter.duration;
}
let tab;
if (id in tabs) {
tab = tabs[id];
} else {
tab = {
windowId: 0,
host: id,
dispatchCount: 0,
duration: 0,
memory: 0,
children: [],
};
tabs[id] = tab;
}
tab.dispatchCount += dispatchCount;
tab.duration += duration;
}
}
return { tabs, date: Cu.now() };
},
/**
* Update the internal state.
*
* @return {Promise}
*/
async update() {
// If the buffer is empty, add one value for bootstraping purposes.
if (!this._buffer.length) {
this._latest = await this._promiseSnapshot();
this._buffer.push(this._latest);
await wait(BUFFER_SAMPLING_RATE_MS * 1.1);
}
let now = Cu.now();
// If we haven't sampled in a while, add a sample to the buffer.
let latestInBuffer = this._buffer[this._buffer.length - 1];
let deltaT = now - latestInBuffer.date;
if (deltaT > BUFFER_SAMPLING_RATE_MS) {
this._latest = await this._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();
}
},
// We can only know asynchronously if an origin is matched by the tracking
// protection list, so we cache the result for faster future lookups.
_trackingState: new Map(),
isTracker(host) {
if (!this._trackingState.has(host)) {
// Temporarily set to false to avoid doing several lookups if a site has
// several subframes on the same domain.
this._trackingState.set(host, false);
if (host.startsWith("about:") || host.startsWith("moz-nullprincipal")) {
return false;
}
let uri = Services.io.newURI("http://" + host);
let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
Ci.nsIURIClassifier
);
let feature = classifier.getFeatureByName("tracking-protection");
if (!feature) {
return false;
}
classifier.asyncClassifyLocalWithFeatures(
uri,
[feature],
Ci.nsIUrlClassifierFeature.blocklist,
list => {
if (list.length) {
this._trackingState.set(host, true);
}
}
);
}
return this._trackingState.get(host);
},
getCounters() {
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 previous = this._buffer[Math.max(this._buffer.length - 2, 0)].tabs;
let current = this._latest.tabs;
let counters = [];
for (let id of Object.keys(current)) {
let tab = current[id];
let oldest;
for (let index = 0; index <= this._buffer.length - 2; ++index) {
if (id in this._buffer[index].tabs) {
oldest = this._buffer[index].tabs[id];
break;
}
}
let prev = previous[id];
let host = tab.host;
let type = "other";
let name = `${host} (${id})`;
let image = "chrome://global/skin/icons/defaultFavicon.svg";
let found = tabFinder.get(parseInt(id));
if (found) {
if (found.tabbrowser) {
name = found.tab.getAttribute("label");
image = found.tab.getAttribute("image");
type = "tab";
} else {
name = {
id: "preloaded-tab",
title: found.tab.linkedBrowser.contentTitle,
};
}
} else if (id == 1) {
name = BRAND_NAME;
image = "chrome://branding/content/icon32.png";
type = "browser";
} else if (/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(host)) {
let addon = WebExtensionPolicy.getByHostname(host);
if (!addon) {
continue;
}
name = `${addon.name} (${addon.id})`;
image =
AddonManager.getPreferredIconURL(
addon.extension.manifest,
32,
window
) || "chrome://mozapps/skin/extensions/extension.svg";
type = gSystemAddonIds.has(addon.id) ? "system-addon" : "addon";
} else if (id == 0 && !tab.isWorker) {
name = { id: "ghost-windows" };
}
if (
type != "tab" &&
type != "addon" &&
!Services.prefs.getBoolPref(
"toolkit.aboutPerformance.showInternals",
false
)
) {
continue;
}
// Create a map of all the child items from the previous time we read the
// counters, indexed by counterId so that we can quickly find the previous
// value for any subitem.
let prevChildren = new Map();
if (prev) {
for (let child of prev.children) {
prevChildren.set(child.counterId, child);
}
}
// For each subitem, create a new object including the deltas since the previous time.
let children = tab.children.map(child => {
let {
host,
dispatchCount,
duration,
memory,
isWorker,
counterId,
} = child;
let dispatchesSincePrevious = dispatchCount;
let durationSincePrevious = duration;
if (prevChildren.has(counterId)) {
let prevCounter = prevChildren.get(counterId);
dispatchesSincePrevious -= prevCounter.dispatchCount;
durationSincePrevious -= prevCounter.duration;
prevChildren.delete(counterId);
}
return {
host,
dispatchCount,
duration,
isWorker,
memory,
dispatchesSincePrevious,
durationSincePrevious,
};
});
// Any item that remains in prevChildren is a subitem that no longer
// exists in the current sample; remember the values of its counters
// so that the values don't go down for the parent item.
tab.dispatchesFromFormerChildren =
(prev && prev.dispatchesFromFormerChildren) || 0;
tab.durationFromFormerChildren =
(prev && prev.durationFromFormerChildren) || 0;
for (let [, counter] of prevChildren) {
tab.dispatchesFromFormerChildren += counter.dispatchCount;
tab.durationFromFormerChildren += counter.duration;
}
// Create the object representing the counters of the parent item including
// the deltas from the previous times.
let dispatches = tab.dispatchCount + tab.dispatchesFromFormerChildren;
let duration = tab.duration + tab.durationFromFormerChildren;
let durationSincePrevious = NaN;
let dispatchesSincePrevious = NaN;
let dispatchesSinceStartOfBuffer = NaN;
let durationSinceStartOfBuffer = NaN;
if (prev) {
durationSincePrevious =
duration - prev.duration - (prev.durationFromFormerChildren || 0);
dispatchesSincePrevious =
dispatches -
prev.dispatchCount -
(prev.dispatchesFromFormerChildren || 0);
}
if (oldest) {
dispatchesSinceStartOfBuffer =
dispatches -
oldest.dispatchCount -
(oldest.dispatchesFromFormerChildren || 0);
durationSinceStartOfBuffer =
duration - oldest.duration - (oldest.durationFromFormerChildren || 0);
}
counters.push({
id,
name,
image,
type,
memory: tab.memory,
totalDispatches: dispatches,
totalDuration: duration,
durationSincePrevious,
dispatchesSincePrevious,
durationSinceStartOfBuffer,
dispatchesSinceStartOfBuffer,
children,
});
}
return counters;
},
getMaxEnergyImpact(counters) {
return Math.max(
...counters.map(c => {
return Control._computeEnergyImpact(
c.dispatchesSincePrevious,
c.durationSincePrevious
);
})
);
},
};
var View = {
_fragment: document.createDocumentFragment(),
async commit() {
let tbody = document.getElementById("dispatch-tbody");
// Force translation to happen before we insert the new content in the DOM
// to avoid flicker when resizing.
await document.l10n.translateFragment(this._fragment);
// Pause the DOMLocalization mutation observer, or the already translated
// content will be translated a second time at the next tick.
document.l10n.pauseObserving();
while (tbody.firstChild) {
tbody.firstChild.remove();
}
tbody.appendChild(this._fragment);
document.l10n.resumeObserving();
this._fragment = document.createDocumentFragment();
},
insertAfterRow(row) {
row.parentNode.insertBefore(this._fragment, row.nextSibling);
this._fragment = document.createDocumentFragment();
},
displayEnergyImpact(elt, energyImpact, maxEnergyImpact) {
if (!energyImpact) {
elt.textContent = "";
elt.style.setProperty("--bar-width", 0);
} else {
let impact;
let barWidth;
const mediumEnergyImpact = 25;
if (energyImpact < 1) {
impact = "low";
// Width 0-10%.
barWidth = 10 * energyImpact;
} else if (energyImpact < mediumEnergyImpact) {
impact = "medium";
// Width 10-50%.
barWidth = (10 + 2 * energyImpact) * (5 / 6);
} else {
impact = "high";
// Width 50-100%.
let energyImpactFromZero = energyImpact - mediumEnergyImpact;
if (maxEnergyImpact > 100) {
barWidth =
50 +
(energyImpactFromZero / (maxEnergyImpact - mediumEnergyImpact)) *
50;
} else {
barWidth = 50 + energyImpactFromZero * (2 / 3);
}
}
document.l10n.setAttributes(elt, "energy-impact-" + impact, {
value: energyImpact,
});
if (maxEnergyImpact != -1) {
elt.style.setProperty("--bar-width", barWidth);
}
}
},
appendRow(
name,
energyImpact,
memory,
tooltip,
type,
maxEnergyImpact = -1,
image = ""
) {
let row = document.createElement("tr");
let elt = document.createElement("td");
if (typeof name == "string") {
elt.textContent = name;
} else if (name.title) {
document.l10n.setAttributes(elt, name.id, { title: name.title });
} else {
document.l10n.setAttributes(elt, name.id);
}
if (image) {
elt.style.backgroundImage = `url('${image}')`;
}
if (["subframe", "tracker", "worker"].includes(type)) {
elt.classList.add("indent");
} else {
elt.classList.add("root");
}
if (["tracker", "worker"].includes(type)) {
elt.classList.add(type);
}
row.appendChild(elt);
elt = document.createElement("td");
let typeLabelType = type == "system-addon" ? "addon" : type;
document.l10n.setAttributes(elt, "type-" + typeLabelType);
row.appendChild(elt);
elt = document.createElement("td");
elt.classList.add("energy-impact");
this.displayEnergyImpact(elt, energyImpact, maxEnergyImpact);
row.appendChild(elt);
elt = document.createElement("td");
if (!memory) {
elt.textContent = "";
} else {
let unit = "KB";
memory = Math.ceil(memory / 1024);
if (memory > 1024) {
memory = Math.ceil((memory / 1024) * 10) / 10;
unit = "MB";
if (memory > 1024) {
memory = Math.ceil((memory / 1024) * 100) / 100;
unit = "GB";
}
}
document.l10n.setAttributes(elt, "size-" + unit, { value: memory });
}
row.appendChild(elt);
if (tooltip) {
for (let key of ["dispatchesSincePrevious", "durationSincePrevious"]) {
if (Number.isNaN(tooltip[key]) || tooltip[key] < 0) {
tooltip[key] = "";
}
}
document.l10n.setAttributes(row, "item", tooltip);
}
elt = document.createElement("td");
if (type == "tab") {
let img = document.createElement("span");
img.className = "action-icon close-icon";
document.l10n.setAttributes(img, "close-tab");
elt.appendChild(img);
} else if (type == "addon") {
let img = document.createElement("span");
img.className = "action-icon addon-icon";
document.l10n.setAttributes(img, "show-addon");
elt.appendChild(img);
}
row.appendChild(elt);
this._fragment.appendChild(row);
return row;
},
};
var Control = {
_openItems: new Set(),
_sortOrder: "",
_removeSubtree(row) {
while (
row.nextSibling &&
row.nextSibling.firstChild.classList.contains("indent")
) {
row.nextSibling.remove();
}
},
init() {
let tbody = document.getElementById("dispatch-tbody");
tbody.addEventListener("click", event => {
this._updateLastMouseEvent();
this._handleActivate(event.target);
});
tbody.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") {
this._handleActivate(event.target);
}
});
// Select the tab of double clicked items.
tbody.addEventListener("dblclick", event => {
let id = parseInt(event.target.parentNode.windowId);
if (isNaN(id)) {
return;
}
let found = tabFinder.get(id);
if (!found || !found.tabbrowser) {
return;
}
let { tabbrowser, tab } = found;
tabbrowser.selectedTab = tab;
tabbrowser.ownerGlobal.focus();
});
tbody.addEventListener("mousemove", () => {
this._updateLastMouseEvent();
});
window.addEventListener("visibilitychange", event => {
if (!document.hidden) {
this._updateDisplay(true);
}
});
document
.getElementById("dispatch-thead")
.addEventListener("click", async event => {
if (!event.target.classList.contains("clickable")) {
return;
}
if (this._sortOrder) {
let [column, direction] = this._sortOrder.split("_");
const td = document.getElementById(`column-${column}`);
td.classList.remove(direction);
}
const columnId = event.target.id;
if (columnId == "column-type") {
this._sortOrder =
this._sortOrder == "type_asc" ? "type_desc" : "type_asc";
} else if (columnId == "column-energy-impact") {
this._sortOrder =
this._sortOrder == "energy-impact_desc"
? "energy-impact_asc"
: "energy-impact_desc";
} else if (columnId == "column-memory") {
this._sortOrder =
this._sortOrder == "memory_desc" ? "memory_asc" : "memory_desc";
} else if (columnId == "column-name") {
this._sortOrder =
this._sortOrder == "name_asc" ? "name_desc" : "name_asc";
}
let direction = this._sortOrder.split("_")[1];
event.target.classList.add(direction);
await this._updateDisplay(true);
});
},
_lastMouseEvent: 0,
_updateLastMouseEvent() {
this._lastMouseEvent = Date.now();
},
_handleActivate(target) {
// Handle showing or hiding subitems of a row.
if (target.classList.contains("twisty")) {
let row = target.parentNode.parentNode;
let id = row.windowId;
if (target.classList.toggle("open")) {
target.setAttribute("aria-expanded", "true");
this._openItems.add(id);
this._showChildren(row);
View.insertAfterRow(row);
} else {
target.setAttribute("aria-expanded", "false");
this._openItems.delete(id);
this._removeSubtree(row);
}
return;
}
// Handle closing a tab.
if (target.classList.contains("close-icon")) {
let row = target.parentNode.parentNode;
let id = parseInt(row.windowId);
let found = tabFinder.get(id);
if (!found || !found.tabbrowser) {
return;
}
let { tabbrowser, tab } = found;
tabbrowser.removeTab(tab);
this._removeSubtree(row);
row.remove();
return;
}
// Handle opening addon details.
if (target.classList.contains("addon-icon")) {
let row = target.parentNode.parentNode;
let id = row.windowId;
let parentWin =
window.docShell.browsingContext.embedderElement.ownerGlobal;
parentWin.BrowserOpenAddonsMgr(
"addons://detail/" + encodeURIComponent(id)
);
return;
}
// Handle selection changes.
let row = target.parentNode;
if (this.selectedRow) {
this.selectedRow.removeAttribute("selected");
}
if (row.windowId) {
row.setAttribute("selected", "true");
this.selectedRow = row;
} else if (this.selectedRow) {
this.selectedRow = null;
}
},
async update() {
await State.update();
if (document.hidden) {
return;
}
await wait(0);
await this._updateDisplay();
},
// The force parameter can force a full update even when the mouse has been
// moved recently or when an element like a Twisty button is focused.
async _updateDisplay(force = false) {
let counters = State.getCounters();
let maxEnergyImpact = State.getMaxEnergyImpact(counters);
let focusedEl;
// If the mouse has been moved recently, update the data displayed
// without moving any item to avoid the risk of users clicking an action
// button for the wrong item.
// Memory use is unlikely to change dramatically within a few seconds, so
// it's probably fine to not update the Memory column in this case.
if (
!force &&
Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
) {
let energyImpactPerId = new Map();
for (let {
id,
dispatchesSincePrevious,
durationSincePrevious,
} of counters) {
let energyImpact = this._computeEnergyImpact(
dispatchesSincePrevious,
durationSincePrevious
);
energyImpactPerId.set(id, energyImpact);
}
let row = document.getElementById("dispatch-tbody").firstChild;
while (row) {
if (row.windowId && energyImpactPerId.has(row.windowId)) {
// We update the value in the Energy Impact column, but don't
// update the children, as if the child count changes there's a
// risk of making other rows move up or down.
const kEnergyImpactColumn = 2;
let elt = row.childNodes[kEnergyImpactColumn];
View.displayEnergyImpact(
elt,
energyImpactPerId.get(row.windowId),
maxEnergyImpact
);
}
row = row.nextSibling;
}
return;
}
let selectedId = -1;
// Reset the selectedRow field and the _openItems set each time we redraw
// to avoid keeping forever references to closed window ids.
if (this.selectedRow) {
selectedId = this.selectedRow.windowId;
this.selectedRow = null;
}
let openItems = this._openItems;
this._openItems = new Set();
// Preserving a focus target if it is located within the page content
let focusedId;
const isFocusable = document.activeElement.classList.contains("twisty");
if (document.hasFocus() && isFocusable) {
focusedId = document.activeElement.parentNode.parentNode.windowId;
}
counters = this._sortCounters(counters);
for (let {
id,
name,
image,
type,
totalDispatches,
dispatchesSincePrevious,
memory,
totalDuration,
durationSincePrevious,
children,
} of counters) {
let row = View.appendRow(
name,
this._computeEnergyImpact(
dispatchesSincePrevious,
durationSincePrevious
),
memory,
{
totalDispatches,
totalDuration: Math.ceil(totalDuration / 1000),
dispatchesSincePrevious,
durationSincePrevious: Math.ceil(durationSincePrevious / 1000),
},
type,
maxEnergyImpact,
image
);
row.windowId = id;
if (id == selectedId) {
row.setAttribute("selected", "true");
this.selectedRow = row;
}
if (!children.length) {
continue;
}
// Show the twisty image as a disclosure button.
let elt = row.firstChild;
let img = document.createElement("span");
img.className = "twisty";
img.setAttribute("role", "button");
img.setAttribute("tabindex", "0");
img.setAttribute("aria-label", name);
let open = openItems.has(id);
if (open) {
img.classList.add("open");
img.setAttribute("aria-expanded", "true");
this._openItems.add(id);
} else {
img.setAttribute("aria-expanded", "false");
}
if (id === focusedId) {
focusedEl = img;
}
// If there's an l10n id on our <td> node, any image we add will be
// removed during localization, so move the l10n id to a <span>
let l10nAttrs = document.l10n.getAttributes(elt);
if (l10nAttrs.id) {
let span = document.createElement("span");
document.l10n.setAttributes(span, l10nAttrs.id, l10nAttrs.args);
elt.removeAttribute("data-l10n-id");
elt.removeAttribute("data-l10n-args");
elt.insertBefore(span, elt.firstChild);
}
elt.insertBefore(img, elt.firstChild);
row._children = children;
if (open) {
this._showChildren(row);
}
}
await View.commit();
if (focusedEl) {
focusedEl.focus();
}
},
_showChildren(row) {
let children = row._children;
children.sort(
(a, b) => b.dispatchesSincePrevious - a.dispatchesSincePrevious
);
for (let row of children) {
let host = row.host.replace(/^blob:https?:\/\//, "");
let type = "subframe";
if (State.isTracker(host)) {
type = "tracker";
}
if (row.isWorker) {
type = "worker";
}
View.appendRow(
row.host,
this._computeEnergyImpact(
row.dispatchesSincePrevious,
row.durationSincePrevious
),
row.memory,
{
totalDispatches: row.dispatchCount,
totalDuration: Math.ceil(row.duration / 1000),
dispatchesSincePrevious: row.dispatchesSincePrevious,
durationSincePrevious: Math.ceil(row.durationSincePrevious / 1000),
},
type
);
}
},
_computeEnergyImpact(dispatches, duration) {
// 'Dispatches' doesn't make sense to users, and it's difficult to present
// two numbers in a meaningful way, so we need to somehow aggregate the
// dispatches and duration values we have.
// The current formula to aggregate the numbers assumes that the cost of
// a dispatch is equivalent to 1ms of CPU time.
// Dividing the result by the sampling interval and by 10 gives a number that
// looks like a familiar percentage to users, as fullying using one core will
// result in a number close to 100.
let energyImpact =
Math.max(duration || 0, dispatches * 1000) / UPDATE_INTERVAL_MS / 10;
// Keep only 2 digits after the decimal point.
return Math.ceil(energyImpact * 100) / 100;
},
_getTypeWeight(type) {
let weights = {
tab: 3,
addon: 2,
"system-addon": 1,
};
return weights[type] || 0;
},
_sortCounters(counters) {
return counters.sort((a, b) => {
// Force 'Recently Closed Tabs' to be always at the bottom, because it'll
// never be actionable.
if (a.name.id && a.name.id == "ghost-windows") {
return 1;
}
if (this._sortOrder) {
let res;
let [column, order] = this._sortOrder.split("_");
switch (column) {
case "memory":
res = a.memory - b.memory;
break;
case "type":
if (a.type != b.type) {
res = this._getTypeWeight(b.type) - this._getTypeWeight(a.type);
} else {
res = String.prototype.localeCompare.call(a.name, b.name);
}
break;
case "name":
res = String.prototype.localeCompare.call(a.name, b.name);
break;
case "energy-impact":
res =
this._computeEnergyImpact(
a.dispatchesSincePrevious,
a.durationSincePrevious
) -
this._computeEnergyImpact(
b.dispatchesSincePrevious,
b.durationSincePrevious
);
break;
default:
res = String.prototype.localeCompare.call(a.name, b.name);
}
if (order == "desc") {
res = -1 * res;
}
return res;
}
// Note: _computeEnergyImpact uses UPDATE_INTERVAL_MS which doesn't match
// the time between the most recent sample and the start of the buffer,
// BUFFER_DURATION_MS would be better, but the values is never displayed
// so this is OK.
let aEI = this._computeEnergyImpact(
a.dispatchesSinceStartOfBuffer,
a.durationSinceStartOfBuffer
);
let bEI = this._computeEnergyImpact(
b.dispatchesSinceStartOfBuffer,
b.durationSinceStartOfBuffer
);
if (aEI != bEI) {
return bEI - aEI;
}
// a.name is sometimes an object, so we can't use a.name.localeCompare.
return String.prototype.localeCompare.call(a.name, b.name);
});
},
};
window.onload = async function() {
Control.init();
let addons = await AddonManager.getAddonsByTypes(["extension"]);
for (let addon of addons) {
if (addon.isSystem) {
gSystemAddonIds.add(addon.id);
}
}
await Control.update();
window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
};