Bug 1632794 - Bare bones about:processes;r=fluent-reviewers,florian,Pike

This is a very first iteration of about:processes, so that people who actually need the tool can start using it immediately and provide feedback.

Differential Revision: https://phabricator.services.mozilla.com/D72617
This commit is contained in:
David Teller 2020-05-05 22:19:44 +00:00
parent ab091e0ddf
commit 985b3f945a
9 changed files with 1023 additions and 0 deletions

View File

@ -101,6 +101,8 @@ static const RedirEntry kRedirMap[] = {
nsIAboutModule::ALLOW_SCRIPT},
{"plugins", "chrome://global/content/plugins.html",
nsIAboutModule::URI_MUST_LOAD_IN_CHILD},
{"processes", "chrome://global/content/aboutProcesses.html",
nsIAboutModule::ALLOW_SCRIPT},
// about:serviceworkers always wants to load in the parent process because
// when dom.serviceWorkers.parent_intercept is set to true (the new default)
// then the only place nsIServiceWorkerManager has any data is in the

View File

@ -36,6 +36,8 @@ if defined('MOZ_CRASHREPORTER'):
about_pages.append('crashes')
if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android':
about_pages.append('profiles')
if defined('NIGHTLY_BUILD'):
about_pages.append('processes')
Headers = ['/docshell/build/nsDocShellModule.h']

View File

@ -0,0 +1,157 @@
/* 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/. */
@import url("chrome://global/skin/in-content/common.css");
html {
background-color: var(--in-content-page-background);
}
body {
overflow-x: hidden;
}
#process-table {
-moz-user-select: none;
font-size: 1em;
border-spacing: 0;
background-color: var(--in-content-box-background);
margin: 0;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
min-width: 40em;
}
/* Avoid scrolling the header */
#process-tbody {
display: block;
margin-top: 2em;
}
#process-thead {
position: fixed;
z-index: 1;
height: 2em;
border-bottom: 1px solid var(--in-content-border-color);
min-width: 40em;
background-color: var(--in-content-box-background);
}
tr {
display: table;
table-layout: fixed;
width: 100%;
}
td:nth-child(1) {
width: 6%;
}
/* At least one column needs to have a flexible width,
so no width specified for td:nth-child(2) */
td:nth-child(3) {
width: 10%;
}
td:nth-child(4) {
width: 10%;
}
td:nth-child(5) {
width: 10%;
}
td:nth-child(6) {
width: 10%;
}
td:nth-child(7) {
width: 2%;
}
#process-thead > tr {
height: inherit;
}
#process-thead > tr > td {
border: none;
background-color: var(--in-content-button-background);
}
#process-thead > tr > td:not(:first-child) {
border-inline-start-width: 1px;
border-inline-start-style: solid;
border-image: linear-gradient(transparent 0%, transparent 20%, var(--in-content-box-border-color) 20%, var(--in-content-box-border-color) 80%, transparent 80%, transparent 100%) 1 1;
border-bottom: 1px solid var(--in-content-border-color);
}
td {
padding: 5px 10px;
min-height: 2em;
color: var(--in-content-text-color);
max-width: 70vw;
overflow: hidden;
white-space: nowrap;
}
#process-tbody > tr > td:first-child {
text-overflow: ellipsis;
}
.twisty {
margin-inline: -10px 0px;
padding-inline: 18px;
position: relative;
}
/* Putting the background image in a positioned pseudo element lets us
* use CSS transforms on the background image, which we need for rtl. */
.twisty::before {
content: url("chrome://global/skin/icons/twisty-collapsed.svg");
position: absolute;
display: block;
line-height: 50%;
top: 4px; /* Half the image's height */
left: 0;
width: 100%;
text-align: center;
-moz-context-properties: fill;
fill: currentColor;
}
.twisty:dir(rtl)::before {
transform: scaleX(-1);
}
.twisty.open::before {
content: url("chrome://global/skin/icons/twisty-expanded.svg");
}
#process-tbody > tr > td.indent {
padding-inline: 36px 0;
}
#process-tbody > tr[selected] > td {
background-color: var(--in-content-item-selected);
color: var(--in-content-selected-text);
}
#process-tbody > tr:hover {
background-color: var(--in-content-item-hover);
}
.clickable {
background-repeat: no-repeat;
background-position: right 4px center;
}
.clickable:dir(rtl) {
background-position-x: left 4px;
}
.asc {
background-image: url(chrome://global/skin/icons/arrow-up-12.svg);
-moz-context-properties: fill;
fill: currentColor;
}
.desc {
background-image: url(chrome://global/skin/icons/arrow-dropdown-12.svg);
-moz-context-properties: fill;
fill: currentColor;
}
#process-thead > tr > td.clickable:hover {
background-color: var(--in-content-button-background-hover);
}
#process-thead > tr > td.clickable:active {
background-color: var(--in-content-button-background-active);
}
#process-tbody > tr.process {
font-weight: bold;
}
#process-tbody > tr.thread {
font-size-adjust: 0.5;
}

View File

@ -0,0 +1,18 @@
# 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/.
# Page title
about-processes-title = Process Manager
## Column headers
about-processes-column-id = Id
about-processes-column-type = Type
about-processes-column-name = Name
about-processes-column-memory-resident = Memory (Resident)
about-processes-column-memory-virtual = Memory (Virtual)
about-processes-column-cpu-user = CPU (User)
about-processes-column-cpu-kernel = CPU (Kernel)
about-processes-column-cpu-threads = CPU (Threads)
about-processes-column-threads = Threads

View File

@ -0,0 +1,31 @@
<!-- 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/. -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src chrome:;img-src data:; object-src 'none'">
<title data-l10n-id="about-processes-title"></title>
<link rel="icon" id="favicon" href="chrome://global/skin/icons/performance.svg">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="localization" href="preview/aboutProcesses.ftl">
<script src="chrome://global/content/aboutProcesses.js"></script>
<link rel="stylesheet" href="chrome://global/content/aboutProcesses.css">
</head>
<body>
<table id="process-table">
<thead id="process-thead">
<tr>
<td class="clickable" id="column-pid" data-l10n-id="about-processes-column-id"></td>
<td class="clickable" id="column-name" data-l10n-id="about-processes-column-name"></td>
<td class="clickable" id="column-memory-resident" data-l10n-id="about-processes-column-memory-resident"></td> <!-- Memory usage. -->
<td class="clickable" id="column-memory-virtual" data-l10n-id="about-processes-column-memory-virtual"></td> <!-- Memory usage. -->
<td class="clickable" id="column-cpu-user" data-l10n-id="about-processes-column-cpu-user"></td> <!-- CPU user. -->
<td class="clickable" id="column-cpu-kernel" data-l10n-id="about-processes-column-cpu-kernel"></td> <!-- CPU kernel. -->
<td class="clickable" id="column-cpu-threads" data-l10n-id="about-processes-column-threads"></td>
</tr>
</thead>
<tbody id="process-tbody"></tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,790 @@
/* -*- 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 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;
const MS_PER_NS = 1000000;
const NS_PER_S = 1000000000;
const ONE_GIGA = 1024 * 1024 * 1024;
const ONE_MEGA = 1024 * 1024;
const ONE_KILO = 1024;
/**
* 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 main = await ChromeUtils.requestProcInfo();
let processes = new Map();
processes.set(main.pid, main);
for (let child of main.children) {
processes.set(child.pid, child);
}
return { processes, 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();
}
},
_getThreadDelta(cur, prev, deltaT) {
let name = cur.name || "???";
let result = {
tid: cur.tid,
name,
// Total amount of CPU used, in ns (user).
totalCpuUser: cur.cpuUser,
slopeCpuUser: null,
// Total amount of CPU used, in ns (kernel).
totalCpuKernel: cur.cpuKernel,
slopeCpuKernel: null,
};
if (!prev) {
return result;
}
if (prev.tid != cur.tid) {
throw new Error("Assertion failed: A thread cannot change tid.");
}
result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
return result;
},
/**
* Compute the delta between two process snapshots.
*
* @param {ProcessSnapshot} cur
* @param {ProcessSnapshot?} prev
* @param {Number?} deltaT A number of nanoseconds elapsed between `cur` and `prev`.
*/
_getProcessDelta(cur, prev, deltaT) {
let result = {
pid: cur.pid,
filename: cur.filename,
totalVirtualMemorySize: cur.virtualMemorySize,
deltaVirtualMemorySize: null,
totalResidentSize: cur.residentSetSize,
deltaResidentSize: null,
totalCpuUser: cur.cpuUser,
slopeCpuUser: null,
totalCpuKernel: cur.cpuKernel,
slopeCpuKernel: null,
type: cur.type,
origin: cur.origin || "",
threads: null,
};
if (!prev) {
result.threads = cur.threads.map(data =>
this._getThreadDelta(data, null, null)
);
return result;
}
if (prev.pid != cur.pid) {
throw new Error("Assertion failed: A process cannot change pid.");
}
if (prev.type != cur.type) {
throw new Error("Assertion failed: A process cannot change type.");
}
let prevThreads = new Map();
for (let thread of prev.threads) {
prevThreads.set(thread.tid, thread);
}
let threads = cur.threads.map(curThread => {
let prevThread = prevThreads.get(curThread.tid);
if (!prevThread) {
return this._getThreadDelta(curThread);
}
return this._getThreadDelta(curThread, prevThread, deltaT);
});
result.deltaVirtualMemorySize =
cur.virtualMemorySize - prev.virtualMemorySize;
result.deltaResidentSize = cur.residentSetSize - prev.residentSetSize;
result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
result.threads = threads;
return result;
},
getCounters() {
// We rebuild the maps during each iteration to make sure that
// we do not maintain references to processes that have been
// shutdown.
let previous = this._buffer[Math.max(this._buffer.length - 2, 0)];
let current = this._latest;
let counters = [];
for (let cur of current.processes.values()) {
// Look for the oldest point of comparison
let oldest = null;
let delta;
for (let index = 0; index <= this._buffer.length - 2; ++index) {
oldest = this._buffer[index].processes.get(cur.pid);
if (oldest) {
// Found it!
break;
}
}
if (oldest) {
// Existing process. Let's display slopes info.
delta = this._getProcessDelta(
cur,
oldest,
(current.date - previous.date) * MS_PER_NS
);
} else {
// New process. Let's display basic info.
delta = this._getProcessDelta(cur, null, null);
}
counters.push(delta);
}
return counters;
},
};
var View = {
_fragment: document.createDocumentFragment(),
async commit() {
let tbody = document.getElementById("process-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);
while (tbody.firstChild) {
tbody.firstChild.remove();
}
tbody.appendChild(this._fragment);
this._fragment = document.createDocumentFragment();
},
insertAfterRow(row) {
row.parentNode.insertBefore(this._fragment, row.nextSibling);
this._fragment = document.createDocumentFragment();
},
/**
* Append a row showing a single process (without its threads).
*
* @param {ProcessDelta} data The data to display.
* @param {bool} isOpen `true` if we're also displaying the threads of this process, `false` otherwise.
* @return {DOMElement} The row displaying the process.
*/
appendProcessRow(data, isOpen) {
let row = document.createElement("tr");
row.classList.add("process");
// Column: pid / twisty image
{
let elt = this._addCell(row, {
content: data.pid,
classes: ["pid", "root"],
});
if (data.threads.length) {
let img = document.createElement("span");
img.classList.add("twisty", "process");
if (isOpen) {
img.classList.add("open");
}
elt.insertBefore(img, elt.firstChild);
}
}
// Column: type
{
let content = data.origin ? `${data.origin} (${data.type})` : data.type;
this._addCell(row, {
content,
classes: ["type"],
});
}
// Column: Resident size
{
let { formatedDelta, formatedValue } = this._formatMemoryAndDelta(
data.totalResidentSize,
data.deltaResidentSize
);
let content = formatedDelta
? `${formatedValue}${formatedDelta}`
: formatedValue;
this._addCell(row, {
content,
classes: ["totalResidentSize"],
});
}
// Column: VM size
{
let { formatedDelta, formatedValue } = this._formatMemoryAndDelta(
data.totalVirtualMemorySize,
data.deltaVirtualMemorySize
);
let content = formatedDelta
? `${formatedValue}${formatedDelta}`
: formatedValue;
this._addCell(row, {
content,
classes: ["totalVirtualMemorySize"],
});
}
// Column: CPU: User
{
let slope = this._formatPercentage(data.slopeCpuUser);
let content = `${slope} (${(
data.totalCpuUser / MS_PER_NS
).toLocaleString(undefined, { maximumFractionDigits: 0 })}ms)`;
this._addCell(row, {
content,
classes: ["cpuUser"],
});
}
// Column: CPU: Kernel
{
let slope = this._formatPercentage(data.slopeCpuKernel);
let content = `${slope} (${(
data.totalCpuKernel / MS_PER_NS
).toLocaleString(undefined, { maximumFractionDigits: 0 })}ms)`;
this._addCell(row, {
content,
classes: ["cpuKernel"],
});
}
// Column: Number of threads
this._addCell(row, {
content: data.threads.length,
classes: ["numberOfThreads"],
});
this._fragment.appendChild(row);
return row;
},
/**
* Append a row showing a single thread.
*
* @param {ThreadDelta} data The data to display.
* @return {DOMElement} The row displaying the thread.
*/
appendThreadRow(data) {
let row = document.createElement("tr");
row.classList.add("thread");
// Column: id
this._addCell(row, {
content: data.tid,
classes: ["tid", "indent"],
});
// Column: filename
this._addCell(row, {
content: data.name,
classes: ["name"],
});
// Column: Resident size (empty)
this._addCell(row, {
content: "",
classes: ["totalResidentSize"],
});
// Column: VM size (empty)
this._addCell(row, {
content: "",
classes: ["totalVirtualMemorySize"],
});
// Column: CPU: User
{
let slope = this._formatPercentage(data.slopeCpuUser);
let text = `${slope} (${(
data.totalCpuUser / MS_PER_NS
).toLocaleString(undefined, { maximumFractionDigits: 0 })} ms)`;
this._addCell(row, {
content: text,
classes: ["cpuUser"],
});
}
// Column: CPU: Kernel
{
let slope = this._formatPercentage(data.slopeCpuKernel);
let text = `${slope} (${(
data.totalCpuKernel / MS_PER_NS
).toLocaleString(undefined, { maximumFractionDigits: 0 })} ms)`;
this._addCell(row, {
content: text,
classes: ["cpuKernel"],
});
}
// Column: Number of threads (empty)
this._addCell(row, {
content: "",
classes: ["numberOfThreads"],
});
this._fragment.appendChild(row);
return row;
},
_addCell(row, { content, classes }) {
let elt = document.createElement("td");
this._setTextAndTooltip(elt, content);
elt.classList.add(...classes);
row.appendChild(elt);
return elt;
},
/**
* Utility method to format an optional percentage.
*
* As a special case, we also handle `null`, which represents the case in which we do
* not have sufficient information to compute a percentage.
*
* @param {Number?} value The value to format. Must be either `null` or a non-negative number.
* A value of 1 means 100%. A value larger than 1 is possible as processes can use several
* cores.
* @return {String}
*/
_formatPercentage(value) {
if (value == null) {
return "?";
}
if (value < 0 || typeof value != "number") {
throw new Error(`Invalid percentage value ${value}`);
}
if (value == 0) {
// Let's make sure that we do not confuse idle and "close to 0%",
// otherwise this results in weird displays.
return "idle";
}
// Now work with actual percentages.
let percentage = value * 100;
if (percentage < 0.01) {
// Tiny percentage, let's display something more useful than "0".
return "~0%";
}
if (percentage < 1) {
// Still a small percentage, but it should fit within 2 digits.
return `${percentage.toLocaleString(undefined, {
maximumFractionDigits: 2,
})}%`;
}
// For other percentages, just return a round number.
return `${Math.round(percentage)}%`;
},
/**
* 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 (value < 0 || typeof value != "number") {
throw new Error(`Invalid memory value ${value}`);
}
if (value >= ONE_GIGA) {
return {
unit: "GB",
amount: Math.ceil((value / ONE_GIGA) * 100) / 100,
};
}
if (value >= ONE_MEGA) {
return {
unit: "MB",
amount: Math.ceil((value / ONE_MEGA) * 100) / 100,
};
}
if (value >= ONE_KILO) {
return {
unit: "KB",
amount: Math.ceil((value / ONE_KILO) * 100) / 100,
};
}
return {
unit: "B",
amount: Math.round(value),
};
},
/**
* Format a value representing an amount of memory and a delta.
*
* @param {Number?} value The value to format. Must be either `null` or a non-negative number.
* @param {Number?} value The delta to format. Must be either `null` or a non-negative number.
* @return {
* {unitValue: "GB" | "MB" | "KB" | B" | "?"},
* formatedValue: string,
* {unitDelta: "GB" | "MB" | "KB" | B" | "?"},
* formatedDelta: string
* }
*/
_formatMemoryAndDelta(value, delta) {
let formatedDelta;
let unitDelta;
if (delta == null) {
formatedDelta == "";
unitDelta = null;
} else if (delta == 0) {
formatedDelta = null;
unitDelta = null;
} else if (delta >= 0) {
let { unit, amount } = this._formatMemory(delta);
formatedDelta = ` (+${amount}${unit})`;
unitDelta = unit;
} else {
let { unit, amount } = this._formatMemory(-delta);
formatedDelta = ` (-${amount}${unit})`;
unitDelta = unit;
}
let { unit: unitValue, amount } = this._formatMemory(value);
return {
unitValue,
unitDelta,
formatedDelta,
formatedValue: `${amount}${unitValue}`,
};
},
_setTextAndTooltip(elt, text, tooltip = text) {
elt.textContent = text;
elt.setAttribute("title", tooltip);
},
};
var Control = {
_openItems: new Set(),
_sortColumn: null,
_sortAscendent: true,
_removeSubtree(row) {
while (row.nextSibling && row.nextSibling.classList.contains("thread")) {
row.nextSibling.remove();
}
},
init() {
let tbody = document.getElementById("process-tbody");
tbody.addEventListener("click", event => {
this._updateLastMouseEvent();
// Handle showing or hiding subitems of a row.
let target = event.target;
if (target.classList.contains("twisty")) {
let row = target.parentNode.parentNode;
let id = row.process.pid;
if (target.classList.toggle("open")) {
this._openItems.add(id);
this._showChildren(row);
View.insertAfterRow(row);
} else {
this._openItems.delete(id);
this._removeSubtree(row);
}
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;
}
});
tbody.addEventListener("mousemove", () => {
this._updateLastMouseEvent();
});
window.addEventListener("visibilitychange", event => {
if (!document.hidden) {
this._updateDisplay(true);
}
});
document
.getElementById("process-thead")
.addEventListener("click", async event => {
if (!event.target.classList.contains("clickable")) {
return;
}
if (this._sortColumn) {
const td = document.getElementById(this._sortColumn);
td.classList.remove("asc");
td.classList.remove("desc");
}
const columnId = event.target.id;
if (columnId == this._sortColumn) {
// Reverse sorting order.
this._sortAscendent = !this._sortAscendent;
} else {
this._sortColumn = columnId;
this._sortAscendent = true;
}
if (this._sortAscendent) {
event.target.classList.remove("desc");
event.target.classList.add("asc");
} else {
event.target.classList.remove("asc");
event.target.classList.add("desc");
}
await this._updateDisplay(true);
});
},
_lastMouseEvent: 0,
_updateLastMouseEvent() {
this._lastMouseEvent = Date.now();
},
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.
async _updateDisplay(force = false) {
if (
!force &&
Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
) {
return;
}
let counters = State.getCounters();
// Reset the selectedRow field and the _openItems set each time we redraw
// to avoid keeping forever references to dead processes.
let openItems = this._openItems;
this._openItems = new Set();
counters = this._sortProcesses(counters);
for (let process of counters) {
let isOpen = openItems.has(process.pid);
let row = View.appendProcessRow(process, isOpen);
row.process = process;
if (isOpen) {
this._openItems.add(process.pid);
this._showChildren(row);
}
}
await View.commit();
},
_showChildren(row) {
let process = row.process;
this._sortThreads(process.threads);
for (let thread of process.threads) {
View.appendThreadRow(thread);
}
},
_sortThreads(threads) {
return threads.sort((a, b) => {
let order;
switch (this._sortColumn) {
case "column-name":
order = a.name.localeCompare(b.name);
break;
case "column-cpu-user":
order = b.slopeCpuUser - a.slopeCpuUser;
if (order == 0) {
order = b.totalCpuUser - a.totalCpuUser;
}
break;
case "column-cpu-kernel":
order = b.slopeCpuKernel - a.slopeCpuKernel;
if (order == 0) {
order = b.totalCpuKernel - a.totalCpuKernel;
}
break;
case "column-cpu-threads":
case "column-memory-resident":
case "column-memory-virtual":
case "column-type":
case "column-pid":
case null:
order = b.tid - a.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-pid":
order = b.pid - a.pid;
break;
case "column-type":
order = String(a.origin).localeCompare(b.origin);
if (order == 0) {
order = String(a.type).localeCompare(b.type);
}
break;
case "column-name":
order = String(a.name).localeCompare(b.name);
break;
case "column-cpu-user":
order = b.slopeCpuUser - a.slopeCpuUser;
if (order == 0) {
order = b.totalCpuUser - a.totalCpuUser;
}
break;
case "column-cpu-kernel":
order = b.slopeCpuKernel - a.slopeCpuKernel;
if (order == 0) {
order = b.totalCpuKernel - a.totalCpuKernel;
}
break;
case "column-cpu-threads":
order = b.threads.length - a.threads.length;
break;
case "column-memory-resident":
order = b.totalResidentSize - a.totalResidentSize;
break;
case "column-memory-virtual":
order = b.totalVirtualMemorySize - a.totalVirtualMemorySize;
break;
case null:
// Default order: browser goes first.
if (a.type == "browser") {
order = -1;
} else if (b.type == "browser") {
order = 1;
}
// Other processes by increasing pid, arbitrarily.
order = b.pid - a.pid;
break;
default:
throw new Error("Unsupported order: " + this._sortColumn);
}
if (!this._sortAscendent) {
order = -order;
}
return order;
});
},
};
window.onload = async function() {
Control.init();
await Control.update();
window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
};

View File

@ -0,0 +1,9 @@
# 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/.
toolkit.jar:
content/global/aboutProcesses.html (content/aboutProcesses.html)
content/global/aboutProcesses.js (content/aboutProcesses.js)
content/global/aboutProcesses.css (content/aboutProcesses.css)
preview/aboutProcesses.ftl (content/aboutProcesses.ftl)

View File

@ -0,0 +1,10 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
with Files('**'):
BUG_COMPONENT = ('Toolkit', 'Performance Monitoring')
JAR_MANIFESTS += ['jar.mn']

View File

@ -126,3 +126,7 @@ if CONFIG['MOZ_BUILD_APP'] == 'browser':
# This is only packaged for browser since corrupt JAR and XPI files tend to be a desktop-OS problem.
if CONFIG['MOZ_BUILD_APP'] == 'browser':
DIRS += ['corroborator']
# about:processes is experimental
if CONFIG['NIGHTLY_BUILD']:
DIRS += ['aboutprocesses']