1615 lines
50 KiB
JavaScript
1615 lines
50 KiB
JavaScript
/* -*- 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(
|
||
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
||
);
|
||
const { AppConstants } = ChromeUtils.importESModule(
|
||
"resource://gre/modules/AppConstants.sys.mjs"
|
||
);
|
||
|
||
ChromeUtils.defineESModuleGetters(this, {
|
||
ContextualIdentityService:
|
||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
||
});
|
||
|
||
ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () {
|
||
return ChromeUtils.importESModule(
|
||
"resource://devtools/client/performance-new/shared/background.sys.mjs"
|
||
);
|
||
});
|
||
|
||
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 "forkServer":
|
||
fluentName = "about-processes-fork-server-process";
|
||
break;
|
||
case "preallocated":
|
||
fluentName = "about-processes-preallocated-process";
|
||
break;
|
||
case "utility":
|
||
fluentName = "about-processes-utility-process";
|
||
break;
|
||
case "inference":
|
||
fluentName = "about-processes-inference-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 profilerButton = document.createElement("span");
|
||
profilerButton.className = "profiler-icon";
|
||
profilerButton.setAttribute("role", "button");
|
||
profilerButton.setAttribute("tabindex", "0");
|
||
profilerButton.setAttribute("aria-pressed", "false");
|
||
document.l10n.setAttributes(
|
||
profilerButton,
|
||
"about-processes-profile-process",
|
||
{ duration: PROFILE_DURATION }
|
||
);
|
||
nameCell.appendChild(profilerButton);
|
||
} 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":
|
||
image = "chrome://branding/content/icon32.png";
|
||
break;
|
||
case "extension":
|
||
image = "chrome://mozapps/skin/extensions/extension.svg";
|
||
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) {
|
||
image = "chrome://global/skin/icons/link.svg";
|
||
}
|
||
}
|
||
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: Actions with Kill button – but not for all processes.
|
||
let actionCell = cpuCell.nextSibling;
|
||
|
||
if (!actionCell.firstChild) {
|
||
let span = document.createElement("span");
|
||
actionCell.appendChild(span);
|
||
}
|
||
|
||
let killButton = actionCell.firstChild;
|
||
|
||
killButton.className = "action-icon";
|
||
|
||
if (data.type != "browser") {
|
||
// 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");
|
||
row.setAttribute("aria-busy", "true");
|
||
} else {
|
||
// Otherwise, let's display the kill button.
|
||
killButton.classList.add("close-icon");
|
||
killButton.setAttribute("role", "button");
|
||
killButton.setAttribute("tabindex", "0");
|
||
let killButtonLabelId = data.type.startsWith("web")
|
||
? "about-processes-shutdown-process"
|
||
: "about-processes-kill-process";
|
||
document.l10n.setAttributes(killButton, killButtonLabelId);
|
||
}
|
||
}
|
||
|
||
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 imgButton = document.createElement("span");
|
||
// Provide markup for an accessible disclosure button:
|
||
imgButton.className = "twisty";
|
||
imgButton.setAttribute("role", "button");
|
||
imgButton.setAttribute("tabindex", "0");
|
||
// Label to include both summary and details texts
|
||
imgButton.setAttribute("aria-labelledby", `${data.pid}-label ${rowId}`);
|
||
if (!imgButton.hasAttribute("aria-expanded")) {
|
||
imgButton.setAttribute("aria-expanded", "false");
|
||
}
|
||
nameCell.appendChild(imgButton);
|
||
|
||
span = document.createElement("span");
|
||
span.setAttribute("id", rowId);
|
||
nameCell.appendChild(span);
|
||
} else {
|
||
// The only thing that can change is the thread count.
|
||
let imgButton = nameCell.firstChild;
|
||
isOpen = imgButton.classList.contains("open");
|
||
span = imgButton.nextSibling;
|
||
}
|
||
document.l10n.setAttributes(span, fluentName, fluentArgs);
|
||
|
||
// Column: action
|
||
let actionCell = nameCell.nextSibling;
|
||
actionCell.className = "action-icon";
|
||
// Note: if/when this action-icon would become a control, ensure it is marked up
|
||
// as a button that is focusable and actionable with keyboard (see killButton)
|
||
|
||
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 actionCell = nameCell.nextSibling;
|
||
|
||
if (!actionCell.firstChild) {
|
||
let span = document.createElement("span");
|
||
actionCell.appendChild(span);
|
||
}
|
||
|
||
let killButton = actionCell.firstChild;
|
||
|
||
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");
|
||
row.setAttribute("aria-busy", "true");
|
||
} else {
|
||
// Otherwise, let's display the kill button.
|
||
killButton.classList.add("close-icon");
|
||
killButton.setAttribute("role", "button");
|
||
killButton.setAttribute("tabindex", "0");
|
||
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);
|
||
// Reset the sorting indication.
|
||
td.setAttribute("aria-sort", "none");
|
||
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);
|
||
event.target.setAttribute(
|
||
"aria-sort",
|
||
this._sortAscendent ? "descending" : "ascending"
|
||
);
|
||
|
||
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.closest("tr");
|
||
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.closest("tr");
|
||
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");
|
||
row.setAttribute("aria-busy", "true");
|
||
|
||
// Avoid continuing to show the tooltip when the button isn't visible.
|
||
target.removeAttribute("data-l10n-id");
|
||
target.removeAttribute("title");
|
||
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");
|
||
row.setAttribute("aria-busy", "true");
|
||
target.removeAttribute("data-l10n-id");
|
||
target.removeAttribute("title");
|
||
|
||
// 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");
|
||
let actionIcon = parentRow.querySelector(".action-item");
|
||
actionIcon?.removeAttribute("data-l10n-id");
|
||
actionIcon?.removeAttribute("title");
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 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");
|
||
target.setAttribute("aria-pressed", "true");
|
||
setTimeout(() => {
|
||
ProfilerPopupBackground.captureProfile("aboutprofiling");
|
||
target.classList.remove("profiler-active");
|
||
target.setAttribute("aria-pressed", "false");
|
||
}, 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);
|
||
};
|