Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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";
// Time in ms before we start changing the sort order again after receiving a
// mousemove event.
const TIME_BEFORE_SORTING_AGAIN = 5000;
// How long we should wait between samples.
const MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS = 1000;
// How often we should update
const UPDATE_INTERVAL_MS = 2000;
const NS_PER_US = 1000;
const NS_PER_MS = 1000 * 1000;
const NS_PER_S = 1000 * 1000 * 1000;
const NS_PER_MIN = NS_PER_S * 60;
const NS_PER_HOUR = NS_PER_MIN * 60;
const NS_PER_DAY = NS_PER_HOUR * 24;
const ONE_GIGA = 1024 * 1024 * 1024;
const ONE_MEGA = 1024 * 1024;
const ONE_KILO = 1024;
const { XPCOMUtils } = ChromeUtils.importESModule(
);
const { AppConstants } = ChromeUtils.importESModule(
);
ChromeUtils.defineESModuleGetters(this, {
ContextualIdentityService:
});
ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () {
return ChromeUtils.importESModule(
);
});
const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
const SHOW_THREADS = Services.prefs.getBoolPref(
"toolkit.aboutProcesses.showThreads"
);
const SHOW_ALL_SUBFRAMES = Services.prefs.getBoolPref(
"toolkit.aboutProcesses.showAllSubframes"
);
const SHOW_PROFILER_ICONS = Services.prefs.getBoolPref(
"toolkit.aboutProcesses.showProfilerIcons"
);
const PROFILE_DURATION = Math.max(
1,
Services.prefs.getIntPref("toolkit.aboutProcesses.profileDuration")
);
/**
* For the time being, Fluent doesn't support duration or memory formats, so we need
* to fetch units from Fluent. To avoid re-fetching at each update, we prefetch these
* units during initialization, asynchronously, and keep them.
*
* @type {
* duration: { ns: String, us: String, ms: String, s: String, m: String, h: String, d: String },
* memory: { B: String, KB: String, MB: String, GB: String, TB: String, PB: String, EB: String }
* }.
*/
let gLocalizedUnits;
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) };
},
};
/**
* Utilities for dealing with state
*/
var State = {
// Store the previous and current samples so they can be compared.
_previous: null,
_latest: null,
async _promiseSnapshot() {
let date = Cu.now();
let main = await ChromeUtils.requestProcInfo();
main.date = date;
let processes = new Map();
processes.set(main.pid, main);
for (let child of main.children) {
child.date = date;
processes.set(child.pid, child);
}
return { processes, date };
},
/**
* Update the internal state.
*
* @return {Promise}
*/
async update(force = false) {
if (
force ||
!this._latest ||
Cu.now() - this._latest.date > MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS
) {
// Replacing this._previous before we are done awaiting
// this._promiseSnapshot can cause this._previous and this._latest to be
// equal for a short amount of time, which can cause test failures when
// a forced update of the display is triggered in the meantime.
let newSnapshot = await this._promiseSnapshot();
this._previous = this._latest;
this._latest = newSnapshot;
}
},
_getThreadDelta(cur, prev, deltaT) {
let result = {
tid: cur.tid,
name: cur.name || `(${cur.tid})`,
// Total amount of CPU used, in ns.
totalCpu: cur.cpuTime,
slopeCpu: null,
active: null,
};
if (!deltaT) {
return result;
}
result.slopeCpu = (result.totalCpu - (prev ? prev.cpuTime : 0)) / deltaT;
result.active =
!!result.slopeCpu || cur.cpuCycleCount > (prev ? prev.cpuCycleCount : 0);
return result;
},
_getDOMWindows(process) {
if (!process.windows) {
return [];
}
if (!process.type == "extensions") {
return [];
}
let windows = process.windows.map(win => {
let tab = tabFinder.get(win.outerWindowId);
let addon =
process.type == "extension"
? WebExtensionPolicy.getByURI(win.documentURI)
: null;
let displayRank;
if (tab) {
displayRank = 1;
} else if (win.isProcessRoot) {
displayRank = 2;
} else if (win.documentTitle) {
displayRank = 3;
} else {
displayRank = 4;
}
return {
outerWindowId: win.outerWindowId,
documentURI: win.documentURI,
documentTitle: win.documentTitle,
isProcessRoot: win.isProcessRoot,
isInProcess: win.isInProcess,
tab,
addon,
// The number of instances we have collapsed.
count: 1,
// A rank used to quickly sort windows.
displayRank,
};
});
// We keep all tabs and addons but we collapse subframes that have the same host.
// A map from host -> subframe.
let collapsible = new Map();
let result = [];
for (let win of windows) {
if (win.tab || win.addon) {
result.push(win);
continue;
}
let prev = collapsible.get(win.documentURI.prePath);
if (prev) {
prev.count += 1;
} else {
collapsible.set(win.documentURI.prePath, win);
result.push(win);
}
}
return result;
},
/**
* Compute the delta between two process snapshots.
*
* @param {ProcessSnapshot} cur
* @param {ProcessSnapshot?} prev
*/
_getProcessDelta(cur, prev) {
let windows = this._getDOMWindows(cur);
let result = {
pid: cur.pid,
childID: cur.childID,
totalRamSize: cur.memory,
deltaRamSize: null,
totalCpu: cur.cpuTime,
slopeCpu: null,
active: null,
type: cur.type,
origin: cur.origin || "",
threads: null,
displayRank: Control._getDisplayGroupRank(cur, windows),
windows,
utilityActors: cur.utilityActors,
// If this process has an unambiguous title, store it here.
title: null,
};
// Attempt to determine a title for this process.
let titles = [
...new Set(
result.windows
.filter(win => win.documentTitle)
.map(win => win.documentTitle)
),
];
if (titles.length == 1) {
result.title = titles[0];
}
if (!prev) {
if (SHOW_THREADS) {
result.threads = cur.threads.map(data => this._getThreadDelta(data));
}
return result;
}
if (prev.pid != cur.pid) {
throw new Error("Assertion failed: A process cannot change pid.");
}
let deltaT = (cur.date - prev.date) * NS_PER_MS;
let threads = null;
if (SHOW_THREADS) {
let prevThreads = new Map();
for (let thread of prev.threads) {
prevThreads.set(thread.tid, thread);
}
threads = cur.threads.map(curThread =>
this._getThreadDelta(curThread, prevThreads.get(curThread.tid), deltaT)
);
}
result.deltaRamSize = cur.memory - prev.memory;
result.slopeCpu = (cur.cpuTime - prev.cpuTime) / deltaT;
result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount;
result.threads = threads;
return result;
},
getCounters() {
tabFinder.update();
let counters = [];
for (let cur of this._latest.processes.values()) {
let prev = this._previous?.processes.get(cur.pid);
counters.push(this._getProcessDelta(cur, prev));
}
return counters;
},
};
var View = {
// Processes, tabs and subframes that we killed during the previous iteration.
// Array<{pid:Number} | {windowId:Number}>
_killedRecently: [],
commit() {
this._killedRecently.length = 0;
let tbody = document.getElementById("process-tbody");
let insertPoint = tbody.firstChild;
let nextRow;
while ((nextRow = this._orderedRows.shift())) {
if (insertPoint && insertPoint === nextRow) {
insertPoint = insertPoint.nextSibling;
} else {
tbody.insertBefore(nextRow, insertPoint);
}
}
if (insertPoint) {
while ((nextRow = insertPoint.nextSibling)) {
this._removeRow(nextRow);
}
this._removeRow(insertPoint);
}
},
// If we are not going to display the updated list of rows, drop references
// to rows that haven't been inserted in the DOM tree.
discardUpdate() {
for (let row of this._orderedRows) {
if (!row.parentNode) {
this._rowsById.delete(row.rowId);
}
}
this._orderedRows = [];
},
insertAfterRow(row) {
let tbody = row.parentNode;
let nextRow;
while ((nextRow = this._orderedRows.pop())) {
tbody.insertBefore(nextRow, row.nextSibling);
}
},
_rowsById: new Map(),
_removeRow(row) {
this._rowsById.delete(row.rowId);
row.remove();
},
_getOrCreateRow(rowId, cellCount) {
let row = this._rowsById.get(rowId);
if (!row) {
row = document.createElement("tr");
while (cellCount--) {
row.appendChild(document.createElement("td"));
}
row.rowId = rowId;
this._rowsById.set(rowId, row);
}
this._orderedRows.push(row);
return row;
},
displayCpu(data, cpuCell, maxSlopeCpu) {
// Put a value < 0% when we really don't want to see a bar as
// otherwise it sometimes appears due to rounding errors when we
// don't have an integer number of pixels.
let barWidth = -0.5;
if (data.slopeCpu == null) {
this._fillCell(cpuCell, {
fluentName: "about-processes-cpu-user-and-kernel-not-ready",
classes: ["cpu"],
});
} else {
let { duration, unit } = this._getDuration(data.totalCpu);
if (data.totalCpu == 0) {
// A thread having used exactly 0ns of CPU time is not possible.
// When we get 0 it means the thread used less than the precision of
// the measurement, and it makes more sense to show '0ms' than '0ns'.
// This is useful on Linux where the minimum non-zero CPU time value
// for threads of child processes is 10ms, and on Windows ARM64 where
// the minimum non-zero value is 16ms.
unit = "ms";
}
let localizedUnit = gLocalizedUnits.duration[unit];
if (data.slopeCpu == 0) {
let fluentName = data.active
? "about-processes-cpu-almost-idle"
: "about-processes-cpu-fully-idle";
this._fillCell(cpuCell, {
fluentName,
fluentArgs: {
total: duration,
unit: localizedUnit,
},
classes: ["cpu"],
});
} else {
this._fillCell(cpuCell, {
fluentName: "about-processes-cpu",
fluentArgs: {
percent: data.slopeCpu,
total: duration,
unit: localizedUnit,
},
classes: ["cpu"],
});
let cpuPercent = data.slopeCpu * 100;
if (maxSlopeCpu > 1) {
cpuPercent /= maxSlopeCpu;
}
// Ensure we always have a visible bar for non-0 values.
barWidth = Math.max(0.5, cpuPercent);
}
}
cpuCell.style.setProperty("--bar-width", barWidth);
},
/**
* Display a row showing a single process (without its threads).
*
* @param {ProcessDelta} data The data to display.
* @param {Number} maxSlopeCpu The largest slopeCpu value.
* @return {DOMElement} The row displaying the process.
*/
displayProcessRow(data, maxSlopeCpu) {
const cellCount = 4;
let rowId = "p:" + data.pid;
let row = this._getOrCreateRow(rowId, cellCount);
row.process = data;
{
let classNames = "process";
if (data.isHung) {
classNames += " hung";
}
row.className = classNames;
}
// Column: Name
let nameCell = row.firstChild;
{
let classNames = [];
let fluentName;
let fluentArgs = {
pid: "" + data.pid, // Make sure that this number is not localized
};
switch (data.type) {
case "web":
fluentName = "about-processes-web-process";
break;
case "webIsolated":
fluentName = "about-processes-web-isolated-process";
fluentArgs.origin = data.origin;
break;
case "webServiceWorker":
fluentName = "about-processes-web-serviceworker";
fluentArgs.origin = data.origin;
break;
case "file":
fluentName = "about-processes-file-process";
break;
case "extension":
fluentName = "about-processes-extension-process";
classNames = ["extensions"];
break;
case "privilegedabout":
fluentName = "about-processes-privilegedabout-process";
break;
case "privilegedmozilla":
fluentName = "about-processes-privilegedmozilla-process";
break;
case "withCoopCoep":
fluentName = "about-processes-with-coop-coep-process";
fluentArgs.origin = data.origin;
break;
case "browser":
fluentName = "about-processes-browser-process";
break;
case "plugin":
fluentName = "about-processes-plugin-process";
break;
case "gmpPlugin":
fluentName = "about-processes-gmp-plugin-process";
break;
case "gpu":
fluentName = "about-processes-gpu-process";
break;
case "vr":
fluentName = "about-processes-vr-process";
break;
case "rdd":
fluentName = "about-processes-rdd-process";
break;
case "socket":
fluentName = "about-processes-socket-process";
break;
case "remoteSandboxBroker":
fluentName = "about-processes-remote-sandbox-broker-process";
break;
case "forkServer":
fluentName = "about-processes-fork-server-process";
break;
case "preallocated":
fluentName = "about-processes-preallocated-process";
break;
case "utility":
fluentName = "about-processes-utility-process";
break;
// The following are probably not going to show up for users
// but let's handle the case anyway to avoid heisenoranges
// during tests in case of a leftover process from a previous
// test.
default:
fluentName = "about-processes-unknown-process";
fluentArgs.type = data.type;
break;
}
// Show container names instead of raw origin attribute suffixes.
if (fluentArgs.origin?.includes("^")) {
let origin = fluentArgs.origin;
let privateBrowsingId, userContextId;
try {
({ privateBrowsingId, userContextId } =
ChromeUtils.createOriginAttributesFromOrigin(origin));
fluentArgs.origin = origin.slice(0, origin.indexOf("^"));
} catch (e) {
// createOriginAttributesFromOrigin can throw NS_ERROR_FAILURE for incorrect origin strings.
}
if (userContextId) {
let identityLabel =
ContextualIdentityService.getUserContextLabel(userContextId);
if (identityLabel) {
fluentArgs.origin += ` — ${identityLabel}`;
}
}
if (privateBrowsingId) {
fluentName += "-private";
}
}
let processNameElement = nameCell;
if (SHOW_PROFILER_ICONS) {
if (!nameCell.firstChild) {
processNameElement = document.createElement("span");
nameCell.appendChild(processNameElement);
let profilerIcon = document.createElement("span");
profilerIcon.className = "profiler-icon";
document.l10n.setAttributes(
profilerIcon,
"about-processes-profile-process",
{ duration: PROFILE_DURATION }
);
nameCell.appendChild(profilerIcon);
} else {
processNameElement = nameCell.firstChild;
}
}
document.l10n.setAttributes(processNameElement, fluentName, fluentArgs);
nameCell.className = ["type", "favicon", ...classNames].join(" ");
nameCell.setAttribute("id", data.pid + "-label");
let image;
switch (data.type) {
case "browser":
case "privilegedabout":
break;
case "extension":
break;
default:
// If all favicons match, pick the shared favicon.
// Otherwise, pick a default icon.
// If some tabs have no favicon, we ignore them.
for (let win of data.windows || []) {
if (!win.tab) {
continue;
}
let favicon = win.tab.tab.getAttribute("image");
if (!favicon) {
// No favicon here, let's ignore the tab.
} else if (!image) {
// Let's pick a first favicon.
// We'll remove it later if we find conflicting favicons.
image = favicon;
} else if (image == favicon) {
// So far, no conflict, keep the favicon.
} else {
// Conflicting favicons, fallback to default.
image = null;
break;
}
}
if (!image) {
}
}
nameCell.style.backgroundImage = `url('${image}')`;
}
// Column: Memory
let memoryCell = nameCell.nextSibling;
{
let formattedTotal = this._formatMemory(data.totalRamSize);
if (data.deltaRamSize) {
let formattedDelta = this._formatMemory(data.deltaRamSize);
this._fillCell(memoryCell, {
fluentName: "about-processes-total-memory-size-changed",
fluentArgs: {
total: formattedTotal.amount,
totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
delta: Math.abs(formattedDelta.amount),
deltaUnit: gLocalizedUnits.memory[formattedDelta.unit],
deltaSign: data.deltaRamSize > 0 ? "+" : "-",
},
classes: ["memory"],
});
} else {
this._fillCell(memoryCell, {
fluentName: "about-processes-total-memory-size-no-change",
fluentArgs: {
total: formattedTotal.amount,
totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
},
classes: ["memory"],
});
}
}
// Column: CPU
let cpuCell = memoryCell.nextSibling;
this.displayCpu(data, cpuCell, maxSlopeCpu);
// Column: Kill button – but not for all processes.
let killButton = cpuCell.nextSibling;
killButton.className = "action-icon";
if (data.type.startsWith("web")) {
// This type of process can be killed.
if (this._killedRecently.some(kill => kill.pid && kill.pid == data.pid)) {
// We're racing between the "kill" action and the visual refresh.
// In a few cases, we could end up with the visual refresh showing
// a process as un-killed while we actually just killed it.
//
// We still want to display the process in case something actually
// went bad and the user needs the information to realize this.
// But we also want to make it visible that the process is being
// killed.
row.classList.add("killed");
} else {
// Otherwise, let's display the kill button.
killButton.classList.add("close-icon");
document.l10n.setAttributes(
killButton,
"about-processes-shutdown-process"
);
}
}
return row;
},
/**
* Display a thread summary row with the thread count and a twisty to
* open/close the list.
*
* @param {ProcessDelta} data The data to display.
* @return {boolean} Whether the full thread list should be displayed.
*/
displayThreadSummaryRow(data) {
const cellCount = 2;
let rowId = "ts:" + data.pid;
let row = this._getOrCreateRow(rowId, cellCount);
row.process = data;
row.className = "thread-summary";
let isOpen = false;
// Column: Name
let nameCell = row.firstChild;
let threads = data.threads;
let activeThreads = new Map();
let activeThreadCount = 0;
for (let t of data.threads) {
if (!t.active) {
continue;
}
++activeThreadCount;
let name = t.name.replace(/ ?#[0-9]+$/, "");
if (!activeThreads.has(name)) {
activeThreads.set(name, { name, slopeCpu: t.slopeCpu, count: 1 });
} else {
let thread = activeThreads.get(name);
thread.count++;
thread.slopeCpu += t.slopeCpu;
}
}
let fluentName, fluentArgs;
if (activeThreadCount) {
let percentFormatter = new Intl.NumberFormat(undefined, {
style: "percent",
minimumSignificantDigits: 1,
});
let threadList = Array.from(activeThreads.values());
threadList.sort((t1, t2) => t2.slopeCpu - t1.slopeCpu);
fluentName = "about-processes-active-threads";
fluentArgs = {
number: threads.length,
active: activeThreadCount,
list: new Intl.ListFormat(undefined, { style: "narrow" }).format(
threadList.map(t => {
let name = t.count > 1 ? `${t.count} × ${t.name}` : t.name;
let percent = Math.round(t.slopeCpu * 1000) / 1000;
if (percent) {
return `${name} ${percentFormatter.format(percent)}`;
}
return name;
})
),
};
} else {
fluentName = "about-processes-inactive-threads";
fluentArgs = {
number: threads.length,
};
}
let span;
if (!nameCell.firstChild) {
nameCell.className = "name indent";
// Create the nodes:
let imgBtn = document.createElement("span");
// Provide markup for an accessible disclosure button:
imgBtn.className = "twisty";
imgBtn.setAttribute("role", "button");
imgBtn.setAttribute("tabindex", "0");
// Label to include both summary and details texts
imgBtn.setAttribute("aria-labelledby", `${data.pid}-label ${rowId}`);
if (!imgBtn.hasAttribute("aria-expanded")) {
imgBtn.setAttribute("aria-expanded", "false");
}
nameCell.appendChild(imgBtn);
span = document.createElement("span");
span.setAttribute("id", rowId);
nameCell.appendChild(span);
} else {
// The only thing that can change is the thread count.
let imgBtn = nameCell.firstChild;
isOpen = imgBtn.classList.contains("open");
span = imgBtn.nextSibling;
}
document.l10n.setAttributes(span, fluentName, fluentArgs);
// Column: action
let actionCell = nameCell.nextSibling;
actionCell.className = "action-icon";
return isOpen;
},
displayDOMWindowRow(data) {
const cellCount = 2;
let rowId = "w:" + data.outerWindowId;
let row = this._getOrCreateRow(rowId, cellCount);
row.win = data;
row.className = "window";
// Column: name
let nameCell = row.firstChild;
let tab = tabFinder.get(data.outerWindowId);
let fluentName;
let fluentArgs = {};
let className;
if (tab && tab.tabbrowser) {
fluentName = "about-processes-tab-name";
fluentArgs.name = tab.tab.label;
className = "tab";
} else if (tab) {
fluentName = "about-processes-preloaded-tab";
className = "preloaded-tab";
} else if (data.count == 1) {
fluentName = "about-processes-frame-name-one";
fluentArgs.url = data.documentURI.spec;
className = "frame-one";
} else {
fluentName = "about-processes-frame-name-many";
fluentArgs.number = data.count;
fluentArgs.shortUrl =
data.documentURI.scheme == "about"
? data.documentURI.spec
: data.documentURI.prePath;
className = "frame-many";
}
this._fillCell(nameCell, {
fluentName,
fluentArgs,
classes: ["name", "indent", "favicon", className],
});
let image = tab?.tab.getAttribute("image");
if (image) {
nameCell.style.backgroundImage = `url('${image}')`;
}
// Column: action
let killButton = nameCell.nextSibling;
killButton.className = "action-icon";
if (data.tab && data.tab.tabbrowser) {
// A tab. We want to be able to close it.
if (
this._killedRecently.some(
kill => kill.windowId && kill.windowId == data.outerWindowId
)
) {
// We're racing between the "kill" action and the visual refresh.
// In a few cases, we could end up with the visual refresh showing
// a window as un-killed while we actually just killed it.
//
// We still want to display the window in case something actually
// went bad and the user needs the information to realize this.
// But we also want to make it visible that the window is being
// killed.
row.classList.add("killed");
} else {
// Otherwise, let's display the kill button.
killButton.classList.add("close-icon");
document.l10n.setAttributes(killButton, "about-processes-shutdown-tab");
}
}
},
utilityActorNameToFluentName(actorName) {
let fluentName;
switch (actorName) {
case "audioDecoder_Generic":
fluentName = "about-processes-utility-actor-audio-decoder-generic";
break;
case "audioDecoder_AppleMedia":
fluentName = "about-processes-utility-actor-audio-decoder-applemedia";
break;
case "audioDecoder_WMF":
fluentName = "about-processes-utility-actor-audio-decoder-wmf";
break;
case "mfMediaEngineCDM":
fluentName = "about-processes-utility-actor-mf-media-engine";
break;
case "jSOracle":
fluentName = "about-processes-utility-actor-js-oracle";
break;
case "windowsUtils":
fluentName = "about-processes-utility-actor-windows-utils";
break;
case "windowsFileDialog":
fluentName = "about-processes-utility-actor-windows-file-dialog";
break;
default:
fluentName = "about-processes-utility-actor-unknown";
break;
}
return fluentName;
},
displayUtilityActorRow(data, parent) {
const cellCount = 2;
// The actor name is expected to be unique within a given utility process.
let rowId = "u:" + parent.pid + data.actorName;
let row = this._getOrCreateRow(rowId, cellCount);
row.actor = data;
row.className = "actor";
// Column: name
let nameCell = row.firstChild;
let fluentName = this.utilityActorNameToFluentName(data.actorName);
let fluentArgs = {};
this._fillCell(nameCell, {
fluentName,
fluentArgs,
classes: ["name", "indent", "favicon"],
});
},
/**
* Display a row showing a single thread.
*
* @param {ThreadDelta} data The data to display.
* @param {Number} maxSlopeCpu The largest slopeCpu value.
*/
displayThreadRow(data, maxSlopeCpu) {
const cellCount = 3;
let rowId = "t:" + data.tid;
let row = this._getOrCreateRow(rowId, cellCount);
row.thread = data;
row.className = "thread";
// Column: name
let nameCell = row.firstChild;
this._fillCell(nameCell, {
fluentName: "about-processes-thread-name-and-id",
fluentArgs: {
name: data.name,
tid: "" + data.tid /* Make sure that this number is not localized */,
},
classes: ["name", "double_indent"],
});
// Column: CPU
this.displayCpu(data, nameCell.nextSibling, maxSlopeCpu);
// Third column (Buttons) is empty, nothing to do.
},
_orderedRows: [],
_fillCell(elt, { classes, fluentName, fluentArgs }) {
document.l10n.setAttributes(elt, fluentName, fluentArgs);
elt.className = classes.join(" ");
},
_getDuration(rawDurationNS) {
if (rawDurationNS <= NS_PER_US) {
return { duration: rawDurationNS, unit: "ns" };
}
if (rawDurationNS <= NS_PER_MS) {
return { duration: rawDurationNS / NS_PER_US, unit: "us" };
}
if (rawDurationNS <= NS_PER_S) {
return { duration: rawDurationNS / NS_PER_MS, unit: "ms" };
}
if (rawDurationNS <= NS_PER_MIN) {
return { duration: rawDurationNS / NS_PER_S, unit: "s" };
}
if (rawDurationNS <= NS_PER_HOUR) {
return { duration: rawDurationNS / NS_PER_MIN, unit: "m" };
}
if (rawDurationNS <= NS_PER_DAY) {
return { duration: rawDurationNS / NS_PER_HOUR, unit: "h" };
}
return { duration: rawDurationNS / NS_PER_DAY, unit: "d" };
},
/**
* Format a value representing an amount of memory.
*
* As a special case, we also handle `null`, which represents the case in which we do
* not have sufficient information to compute an amount of memory.
*
* @param {Number?} value The value to format. Must be either `null` or a non-negative number.
* @return { {unit: "GB" | "MB" | "KB" | B" | "?"}, amount: Number } The formated amount and its
* unit, which may be used for e.g. additional CSS formating.
*/
_formatMemory(value) {
if (value == null) {
return { unit: "?", amount: 0 };
}
if (typeof value != "number") {
throw new Error(`Invalid memory value ${value}`);
}
let abs = Math.abs(value);
if (abs >= ONE_GIGA) {
return {
unit: "GB",
amount: value / ONE_GIGA,
};
}
if (abs >= ONE_MEGA) {
return {
unit: "MB",
amount: value / ONE_MEGA,
};
}
if (abs >= ONE_KILO) {
return {
unit: "KB",
amount: value / ONE_KILO,
};
}
return {
unit: "B",
amount: value,
};
},
};
var Control = {
// The set of all processes reported as "hung" by the process hang monitor.
//
// type: Set<ChildID>
_hungItems: new Set(),
_sortColumn: null,
_sortAscendent: true,
_removeSubtree(row) {
let sibling = row.nextSibling;
while (sibling && !sibling.classList.contains("process")) {
let next = sibling.nextSibling;
if (sibling.classList.contains("thread")) {
View._removeRow(sibling);
}
sibling = next;
}
},
init() {
this._initHangReports();
// Start prefetching units.
this._promisePrefetchedUnits = (async function () {
let [ns, us, ms, s, m, h, d, B, KB, MB, GB, TB, PB, EB] =
await document.l10n.formatValues([
{ id: "duration-unit-ns" },
{ id: "duration-unit-us" },
{ id: "duration-unit-ms" },
{ id: "duration-unit-s" },
{ id: "duration-unit-m" },
{ id: "duration-unit-h" },
{ id: "duration-unit-d" },
{ id: "memory-unit-B" },
{ id: "memory-unit-KB" },
{ id: "memory-unit-MB" },
{ id: "memory-unit-GB" },
{ id: "memory-unit-TB" },
{ id: "memory-unit-PB" },
{ id: "memory-unit-EB" },
]);
return {
duration: { ns, us, ms, s, m, h, d },
memory: { B, KB, MB, GB, TB, PB, EB },
};
})();
let tbody = document.getElementById("process-tbody");
// Single click:
// - show or hide the contents of a twisty;
// - close a process;
// - profile a process;
// - change selection.
tbody.addEventListener("click", event => {
this._updateLastMouseEvent();
this._handleActivate(event.target);
});
// Enter or Space keypress:
// - show or hide the contents of a twisty;
// - close a process;
// - profile a process;
// - change selection.
tbody.addEventListener("keypress", event => {
// Handle showing or hiding subitems of a row, when keyboard is used.
if (event.key === "Enter" || event.key === " ") {
this._handleActivate(event.target);
}
});
// Double click:
// - navigate to tab;
// - navigate to about:addons.
tbody.addEventListener("dblclick", event => {
this._updateLastMouseEvent();
event.stopPropagation();
// Bubble up the doubleclick manually.
for (
let target = event.target;
target && target.getAttribute("id") != "process-tbody";
target = target.parentNode
) {
if (target.classList.contains("tab")) {
// We've clicked on a tab, navigate.
let { tab, tabbrowser } = target.parentNode.win.tab;
tabbrowser.selectedTab = tab;
tabbrowser.ownerGlobal.focus();
return;
}
if (target.classList.contains("extensions")) {
// We've clicked on the extensions process, open or reuse window.
let parentWin =
window.docShell.browsingContext.embedderElement.ownerGlobal;
parentWin.BrowserAddonUI.openAddonsMgr();
return;
}
// Otherwise, proceed.
}
});
tbody.addEventListener("mousemove", () => {
this._updateLastMouseEvent();
});
// Visibility change:
// - stop updating while the user isn't looking;
// - resume updating when the user returns.
window.addEventListener("visibilitychange", () => {
if (!document.hidden) {
this._updateDisplay(true);
}
});
document
.getElementById("process-thead")
.addEventListener("click", async event => {
if (!event.target.classList.contains("clickable")) {
return;
}
// Linux has conventions opposite to Windows and macOS on the direction of arrows
// when sorting.
const platformIsLinux = AppConstants.platform == "linux";
const ascArrow = platformIsLinux ? "arrow-up" : "arrow-down";
const descArrow = platformIsLinux ? "arrow-down" : "arrow-up";
if (this._sortColumn) {
const td = document.getElementById(this._sortColumn);
td.classList.remove(ascArrow, descArrow);
}
const columnId = event.target.id;
if (columnId == this._sortColumn) {
// Reverse sorting order.
this._sortAscendent = !this._sortAscendent;
} else {
this._sortColumn = columnId;
this._sortAscendent = true;
}
event.target.classList.toggle(ascArrow, this._sortAscendent);
event.target.classList.toggle(descArrow, !this._sortAscendent);
await this._updateDisplay(true);
});
},
_lastMouseEvent: 0,
_updateLastMouseEvent() {
this._lastMouseEvent = Date.now();
},
_initHangReports() {
const PROCESS_HANG_REPORT_NOTIFICATION = "process-hang-report";
// Receiving report of a hung child.
// Let's store if for our next update.
let hangReporter = report => {
report.QueryInterface(Ci.nsIHangReport);
this._hungItems.add(report.childID);
};
Services.obs.addObserver(hangReporter, PROCESS_HANG_REPORT_NOTIFICATION);
// Don't forget to unregister the reporter.
window.addEventListener(
"unload",
() => {
Services.obs.removeObserver(
hangReporter,
PROCESS_HANG_REPORT_NOTIFICATION
);
},
{ once: true }
);
},
async update(force = false) {
await State.update(force);
if (document.hidden) {
return;
}
await this._updateDisplay(force);
},
// The force parameter can force a full update even when the mouse has been
// moved recently.
async _updateDisplay(force = false) {
let counters = State.getCounters();
if (this._promisePrefetchedUnits) {
gLocalizedUnits = await this._promisePrefetchedUnits;
this._promisePrefetchedUnits = null;
}
// We reset `_hungItems`, based on the assumption that the process hang
// monitor will inform us again before the next update. Since the process hang monitor
// pings its clients about once per second and we update about once per 2 seconds
// (or more if the mouse moves), we should be ok.
let hungItems = this._hungItems;
this._hungItems = new Set();
counters = this._sortProcesses(counters);
// Stored because it is used when opening the list of threads.
this._maxSlopeCpu = Math.max(...counters.map(process => process.slopeCpu));
let previousProcess = null;
for (let process of counters) {
this._sortDOMWindows(process.windows);
process.isHung = process.childID && hungItems.has(process.childID);
let processRow = View.displayProcessRow(process, this._maxSlopeCpu);
if (process.type != "extension") {
// We do not want to display extensions.
for (let win of process.windows) {
if (SHOW_ALL_SUBFRAMES || win.tab || win.isProcessRoot) {
View.displayDOMWindowRow(win, process);
}
}
}
if (process.type === "utility") {
for (let actor of process.utilityActors) {
View.displayUtilityActorRow(actor, process);
}
}
if (SHOW_THREADS) {
if (View.displayThreadSummaryRow(process)) {
this._showThreads(processRow, this._maxSlopeCpu);
}
}
if (
this._sortColumn == null &&
previousProcess &&
previousProcess.displayRank != process.displayRank
) {
// Add a separation between successive categories of processes.
processRow.classList.add("separate-from-previous-process-group");
}
previousProcess = process;
}
if (
!force &&
Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
) {
// If there has been a recent mouse event, we don't want to reorder,
// add or remove rows so that the table content under the mouse pointer
// doesn't change when the user might be about to click to close a tab
// or kill a process.
// We didn't return earlier because updating CPU and memory values is
// still valuable.
View.discardUpdate();
return;
}
View.commit();
// Reset the selectedRow field if that row is no longer in the DOM
// to avoid keeping forever references to dead processes.
if (this.selectedRow && !this.selectedRow.parentNode) {
this.selectedRow = null;
}
// Used by tests to differentiate full updates from l10n updates.
document.dispatchEvent(new CustomEvent("AboutProcessesUpdated"));
},
_compareCpu(a, b) {
return (
b.slopeCpu - a.slopeCpu || b.active - a.active || b.totalCpu - a.totalCpu
);
},
_showThreads(row, maxSlopeCpu) {
let process = row.process;
this._sortThreads(process.threads);
for (let thread of process.threads) {
View.displayThreadRow(thread, maxSlopeCpu);
}
},
_sortThreads(threads) {
return threads.sort((a, b) => {
let order;
switch (this._sortColumn) {
case "column-name":
order = a.name.localeCompare(b.name) || a.tid - b.tid;
break;
case "column-cpu-total":
order = this._compareCpu(a, b);
break;
case "column-memory-resident":
case null:
order = a.tid - b.tid;
break;
default:
throw new Error("Unsupported order: " + this._sortColumn);
}
if (!this._sortAscendent) {
order = -order;
}
return order;
});
},
_sortProcesses(counters) {
return counters.sort((a, b) => {
let order;
switch (this._sortColumn) {
case "column-name":
order =
String(a.origin).localeCompare(b.origin) ||
String(a.type).localeCompare(b.type) ||
a.pid - b.pid;
break;
case "column-cpu-total":
order = this._compareCpu(a, b);
break;
case "column-memory-resident":
order = b.totalRamSize - a.totalRamSize;
break;
case null:
// Default order: classify processes by group.
order =
a.displayRank - b.displayRank ||
// Other processes are ordered by origin.
String(a.origin).localeCompare(b.origin);
break;
default:
throw new Error("Unsupported order: " + this._sortColumn);
}
if (!this._sortAscendent) {
order = -order;
}
return order;
});
},
_sortDOMWindows(windows) {
return windows.sort((a, b) => {
let order =
a.displayRank - b.displayRank ||
a.documentTitle.localeCompare(b.documentTitle) ||
a.documentURI.spec.localeCompare(b.documentURI.spec);
if (!this._sortAscendent) {
order = -order;
}
return order;
});
},
// Assign a display rank to a process.
//
// The `browser` process comes first (rank 0).
// Then come web tabs (rank 1).
// Then come web frames (rank 2).
// Then come special processes (minus preallocated) (rank 3).
// Then come preallocated processes (rank 4).
_getDisplayGroupRank(data, windows) {
const RANK_BROWSER = 0;
const RANK_WEB_TABS = 1;
const RANK_WEB_FRAMES = 2;
const RANK_UTILITY = 3;
const RANK_PREALLOCATED = 4;
let type = data.type;
switch (type) {
// Browser comes first.
case "browser":
return RANK_BROWSER;
// Web content comes next.
case "webIsolated":
case "webServiceWorker":
case "withCoopCoep": {
if (windows.some(w => w.tab)) {
return RANK_WEB_TABS;
}
return RANK_WEB_FRAMES;
}
// Preallocated processes come last.
case "preallocated":
return RANK_PREALLOCATED;
// "web" is special, as it could be one of:
// - web content currently loading/unloading/...
// - a preallocated process.
case "web":
if (windows.some(w => w.tab)) {
return RANK_WEB_TABS;
}
if (windows.length >= 1) {
return RANK_WEB_FRAMES;
}
// For the time being, we do not display DOM workers
// (and there's no API to get information on them).
// Once the blockers for bug 1663737 have landed, we'll be able
// to find out whether this process has DOM workers. If so, we'll
// count this process as a content process.
return RANK_PREALLOCATED;
// Other special processes before preallocated.
default:
return RANK_UTILITY;
}
},
// Handle events on image controls.
_handleActivate(target) {
if (target.classList.contains("twisty")) {
this._handleTwisty(target);
return;
}
if (target.classList.contains("close-icon")) {
this._handleKill(target);
return;
}
if (target.classList.contains("profiler-icon")) {
this._handleProfiling(target);
return;
}
this._handleSelection(target);
},
// Open/close list of threads.
_handleTwisty(target) {
let row = target.parentNode.parentNode;
if (target.classList.toggle("open")) {
target.setAttribute("aria-expanded", "true");
this._showThreads(row, this._maxSlopeCpu);
View.insertAfterRow(row);
} else {
target.setAttribute("aria-expanded", "false");
this._removeSubtree(row);
}
},
// Kill process/close tab/close subframe.
_handleKill(target) {
let row = target.parentNode;
if (row.process) {
// Kill process immediately.
let pid = row.process.pid;
// Make sure that the user can't click twice on the kill button.
// Otherwise, chaos might ensue. Plus we risk crashing under Windows.
View._killedRecently.push({ pid });
// Discard tab contents and show that the process and all its contents are getting killed.
row.classList.add("killing");
for (
let childRow = row.nextSibling;
childRow && !childRow.classList.contains("process");
childRow = childRow.nextSibling
) {
childRow.classList.add("killing");
let win = childRow.win;
if (win) {
View._killedRecently.push({ pid: win.outerWindowId });
if (win.tab && win.tab.tabbrowser) {
win.tab.tabbrowser.discardBrowser(
win.tab.tab,
/* aForceDiscard = */ true
);
}
}
}
// Finally, kill the process.
const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
Ci.nsIProcessToolsService
);
ProcessTools.kill(pid);
} else if (row.win && row.win.tab && row.win.tab.tabbrowser) {
// This is a tab, close it.
row.win.tab.tabbrowser.removeTab(row.win.tab.tab, {
skipPermitUnload: true,
animate: true,
});
View._killedRecently.push({ outerWindowId: row.win.outerWindowId });
row.classList.add("killing");
// If this was the only root window of the process, show that the process is also getting killed.
if (row.previousSibling.classList.contains("process")) {
let parentRow = row.previousSibling;
let roots = 0;
for (let win of parentRow.process.windows) {
if (win.isProcessRoot) {
roots += 1;
}
}
if (roots <= 1) {
// Yes, we're the only process root, so the process is dying.
//
// It might actually become a preloaded process rather than
// dying. That's an acceptable error. Even if we display incorrectly
// that the process is dying, this error will last only one refresh.
View._killedRecently.push({ pid: parentRow.process.pid });
parentRow.classList.add("killing");
}
}
}
},
// Handle profiling of a process.
_handleProfiling(target) {
if (Services.profiler.IsActive()) {
return;
}
Services.profiler.StartProfiler(
10000000,
1,
["default", "ipcmessages", "power"],
["pid:" + target.parentNode.parentNode.process.pid]
);
target.classList.add("profiler-active");
setTimeout(() => {
ProfilerPopupBackground.captureProfile("aboutprofiling");
target.classList.remove("profiler-active");
}, PROFILE_DURATION * 1000);
},
// Handle selection changes.
_handleSelection(target) {
let row = target.closest("tr");
if (!row) {
return;
}
if (this.selectedRow) {
this.selectedRow.removeAttribute("selected");
if (this.selectedRow.rowId == row.rowId) {
// Clicking the same row again clears the selection.
this.selectedRow = null;
return;
}
}
row.setAttribute("selected", "true");
this.selectedRow = row;
},
};
window.onload = async function () {
Control.init();
// Display immediately the list of processes. CPU values will be missing.
await Control.update();
// After the minimum interval between samples, force an update to show
// valid CPU values asap.
await new Promise(resolve =>
setTimeout(resolve, MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS)
);
await Control.update(true);
// Then update at the normal frequency.
window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
};