1087 lines
34 KiB
JavaScript
1087 lines
34 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
// A bunch of assumptions we make about the behavior of the parent process,
|
|
// and which we use as sanity checks. If Firefox evolves, we will need to
|
|
// update these values.
|
|
// Note that Test Verify can really stress the cpu durations.
|
|
const HARDCODED_ASSUMPTIONS_PROCESS = {
|
|
minimalNumberOfThreads: 6,
|
|
maximalNumberOfThreads: 1000,
|
|
minimalCPUPercentage: 0,
|
|
maximalCPUPercentage: 1000,
|
|
minimalCPUTotalDurationMS: 10,
|
|
maximalCPUTotalDurationMS: 10000000,
|
|
minimalRAMBytesUsage: 1024 * 1024 /* 1 Megabyte */,
|
|
maximalRAMBytesUsage: 1024 * 1024 * 1024 * 1024 * 1 /* 1 Tb */,
|
|
};
|
|
|
|
const HARDCODED_ASSUMPTIONS_THREAD = {
|
|
minimalCPUPercentage: 0,
|
|
maximalCPUPercentage: 100,
|
|
minimalCPUTotalDurationMS: 0,
|
|
maximalCPUTotalDurationMS: 10000000,
|
|
};
|
|
|
|
// How close we accept our rounding up/down.
|
|
const APPROX_FACTOR = 1.51;
|
|
const MS_PER_NS = 1000000;
|
|
|
|
// Wait for `about:processes` to be updated.
|
|
async function promiseAboutProcessesUpdated({ doc, force, tabAboutProcesses }) {
|
|
let startTime = performance.now();
|
|
|
|
let updatePromise = new Promise(resolve => {
|
|
doc.addEventListener("AboutProcessesUpdated", resolve, { once: true });
|
|
});
|
|
|
|
if (force) {
|
|
await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
|
|
info("Forcing about:processes refresh");
|
|
await content.Control.update(/* force = */ true);
|
|
});
|
|
}
|
|
|
|
await updatePromise;
|
|
|
|
// Fluent will update the visible table content during the next
|
|
// refresh driver tick, wait for it.
|
|
// requestAnimationFrame calls us at the begining of the tick, we use
|
|
// dispatchToMainThread to execute our code after the end of it.
|
|
//XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed.
|
|
await new Promise(doc.defaultView.requestAnimationFrame);
|
|
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
|
|
|
ChromeUtils.addProfilerMarker(
|
|
"promiseAboutProcessesUpdated",
|
|
{ startTime, category: "Test" },
|
|
force ? "force" : undefined
|
|
);
|
|
}
|
|
|
|
function promiseProcessDied({ childID }) {
|
|
return new Promise(resolve => {
|
|
let observer = properties => {
|
|
properties.QueryInterface(Ci.nsIPropertyBag2);
|
|
let subjectChildID = properties.get("childID");
|
|
if (subjectChildID == childID) {
|
|
Services.obs.removeObserver(observer, "ipc:content-shutdown");
|
|
resolve();
|
|
}
|
|
};
|
|
Services.obs.addObserver(observer, "ipc:content-shutdown");
|
|
});
|
|
}
|
|
|
|
function isCloseEnough(value, expected) {
|
|
if (value < 0 || expected < 0) {
|
|
throw new Error(`Invalid isCloseEnough(${value}, ${expected})`);
|
|
}
|
|
if (Math.round(value) == Math.round(expected)) {
|
|
return true;
|
|
}
|
|
if (expected == 0) {
|
|
return false;
|
|
}
|
|
let ratio = value / expected;
|
|
if (ratio <= APPROX_FACTOR && ratio >= 1 / APPROX_FACTOR) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getMemoryMultiplier(unit, sign = "+") {
|
|
let multiplier;
|
|
switch (sign) {
|
|
case "+":
|
|
multiplier = 1;
|
|
break;
|
|
case "-":
|
|
multiplier = -1;
|
|
break;
|
|
default:
|
|
throw new Error("Invalid sign: " + sign);
|
|
}
|
|
switch (unit) {
|
|
case "B":
|
|
break;
|
|
case "KB":
|
|
multiplier *= 1024;
|
|
break;
|
|
case "MB":
|
|
multiplier *= 1024 * 1024;
|
|
break;
|
|
case "GB":
|
|
multiplier *= 1024 * 1024 * 1024;
|
|
break;
|
|
case "TB":
|
|
multiplier *= 1024 * 1024 * 1024 * 1024;
|
|
break;
|
|
default:
|
|
throw new Error("Invalid memory unit: " + unit);
|
|
}
|
|
return multiplier;
|
|
}
|
|
|
|
function getTimeMultiplier(unit) {
|
|
switch (unit) {
|
|
case "ns":
|
|
return 1 / (1000 * 1000);
|
|
case "µs":
|
|
return 1 / 1000;
|
|
case "ms":
|
|
return 1;
|
|
case "s":
|
|
return 1000;
|
|
case "m":
|
|
return 60000;
|
|
}
|
|
throw new Error("Invalid time unit: " + unit);
|
|
}
|
|
async function testCpu(element, total, slope, assumptions) {
|
|
info(
|
|
`Testing CPU display ${element.textContent} - ${element.title} vs total ${total}, slope ${slope}`
|
|
);
|
|
let barWidth = getComputedStyle(element).getPropertyValue("--bar-width");
|
|
if (slope) {
|
|
Assert.greater(
|
|
Number.parseFloat(barWidth),
|
|
0,
|
|
"The bar width should be > 0 when there is some CPU use"
|
|
);
|
|
} else {
|
|
Assert.equal(barWidth, "-0.5", "There should be no CPU bar displayed");
|
|
}
|
|
|
|
if (element.textContent == "(measuring)") {
|
|
info("Still measuring");
|
|
return;
|
|
}
|
|
|
|
const CPU_TEXT_CONTENT_REGEXP = /\~0%|idle|[0-9.,]+%|[?]/;
|
|
let extractedPercentage = CPU_TEXT_CONTENT_REGEXP.exec(
|
|
element.textContent
|
|
)[0];
|
|
switch (extractedPercentage) {
|
|
case "idle":
|
|
Assert.equal(slope, 0, "Idle means exactly 0%");
|
|
// Nothing else to do here.
|
|
return;
|
|
case "~0%":
|
|
Assert.ok(slope > 0 && slope < 0.0001);
|
|
break;
|
|
case "?":
|
|
Assert.ok(slope == null);
|
|
// Nothing else to do here.
|
|
return;
|
|
default: {
|
|
// `Number.parseFloat("99%")` returns `99`.
|
|
let computedPercentage = Number.parseFloat(extractedPercentage);
|
|
Assert.ok(
|
|
isCloseEnough(computedPercentage, slope * 100),
|
|
`The displayed approximation of the slope is reasonable: ${computedPercentage} vs ${
|
|
slope * 100
|
|
}`
|
|
);
|
|
// Also, sanity checks.
|
|
Assert.ok(
|
|
computedPercentage / 100 >= assumptions.minimalCPUPercentage,
|
|
`Not too little: ${computedPercentage / 100} >=? ${
|
|
assumptions.minimalCPUPercentage
|
|
} `
|
|
);
|
|
Assert.ok(
|
|
computedPercentage / 100 <= assumptions.maximalCPUPercentage,
|
|
`Not too much: ${computedPercentage / 100} <=? ${
|
|
assumptions.maximalCPUPercentage
|
|
} `
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const CPU_TOOLTIP_REGEXP = /(?:.*: ([0-9.,]+) ?(ns|µs|ms|s|m|h|d))/;
|
|
// Example: "Total CPU time: 4,470ms"
|
|
|
|
let [, extractedTotal, extractedUnit] = CPU_TOOLTIP_REGEXP.exec(
|
|
element.title
|
|
);
|
|
|
|
let totalMS = total / MS_PER_NS;
|
|
let computedTotal =
|
|
// We produce localized numbers, with "," as a thousands separator in en-US builds,
|
|
// but `parseFloat` doesn't understand the ",", so we need to remove it
|
|
// before parsing.
|
|
Number.parseFloat(extractedTotal.replace(/,/g, "")) *
|
|
getTimeMultiplier(extractedUnit);
|
|
Assert.ok(
|
|
isCloseEnough(computedTotal, totalMS),
|
|
`The displayed approximation of the total duration is reasonable: ${computedTotal} vs ${totalMS}`
|
|
);
|
|
Assert.ok(
|
|
totalMS <= assumptions.maximalCPUTotalDurationMS &&
|
|
totalMS >= assumptions.minimalCPUTotalDurationMS,
|
|
`The total number of MS is reasonable ${totalMS}: [${assumptions.minimalCPUTotalDurationMS}, ${assumptions.maximalCPUTotalDurationMS}]`
|
|
);
|
|
}
|
|
|
|
async function testMemory(element, total, delta, assumptions) {
|
|
info(
|
|
`Testing memory display ${element.textContent} - ${element.title} vs total ${total}, delta ${delta}`
|
|
);
|
|
const MEMORY_TEXT_CONTENT_REGEXP = /([0-9.,]+)(TB|GB|MB|KB|B)/;
|
|
// Example: "383.55MB"
|
|
let extracted = MEMORY_TEXT_CONTENT_REGEXP.exec(element.textContent);
|
|
Assert.notEqual(
|
|
extracted,
|
|
null,
|
|
`Can we parse ${element.textContent} with ${MEMORY_TEXT_CONTENT_REGEXP}?`
|
|
);
|
|
let [, extractedTotal, extractedUnit] = extracted;
|
|
|
|
let extractedTotalNumber = Number.parseFloat(extractedTotal);
|
|
Assert.ok(
|
|
extractedTotalNumber > 0,
|
|
`Unitless total memory use is greater than 0: ${extractedTotal}`
|
|
);
|
|
if (extractedUnit != "GB") {
|
|
Assert.ok(
|
|
extractedTotalNumber <= 1024,
|
|
`Unitless total memory use is less than 1024: ${extractedTotal}`
|
|
);
|
|
}
|
|
|
|
// Now check that the conversion was meaningful.
|
|
let computedTotal = getMemoryMultiplier(extractedUnit) * extractedTotalNumber;
|
|
Assert.ok(
|
|
isCloseEnough(computedTotal, total),
|
|
`The displayed approximation of the total amount of memory is reasonable: ${computedTotal} vs ${total}`
|
|
);
|
|
if (!AppConstants.ASAN) {
|
|
// ASAN plays tricks with RAM (e.g. allocates the entirety of virtual memory),
|
|
// which makes this test unrealistic.
|
|
Assert.ok(
|
|
assumptions.minimalRAMBytesUsage <= computedTotal &&
|
|
computedTotal <= assumptions.maximalRAMBytesUsage,
|
|
`The total amount amount of memory is reasonable: ${computedTotal} in [${assumptions.minimalRAMBytesUsage}, ${assumptions.maximalRAMBytesUsage}]`
|
|
);
|
|
}
|
|
|
|
const MEMORY_TOOLTIP_REGEXP = /(?:.*: ([-+]?)([0-9.,]+)(GB|MB|KB|B))?/;
|
|
// Example: "Evolution: -12.5MB"
|
|
extracted = MEMORY_TOOLTIP_REGEXP.exec(element.title);
|
|
Assert.notEqual(
|
|
extracted,
|
|
null,
|
|
`Can we parse ${element.title} with ${MEMORY_TOOLTIP_REGEXP}?`
|
|
);
|
|
let [, extractedDeltaSign, extractedDeltaTotal, extractedDeltaUnit] =
|
|
extracted;
|
|
if (extractedDeltaSign == null) {
|
|
Assert.equal(delta || 0, 0);
|
|
return;
|
|
}
|
|
let deltaTotalNumber = Number.parseFloat(
|
|
// Remove the thousands separator that breaks parseFloat.
|
|
extractedDeltaTotal.replace(/,/g, "")
|
|
);
|
|
// Note: displaying 1024KB can happen if the value is slightly less than
|
|
// 1024*1024B but rounded to 1024KB.
|
|
Assert.ok(
|
|
deltaTotalNumber > 0 && deltaTotalNumber <= 1024,
|
|
`Unitless delta memory use is in (0, 1024): ${extractedDeltaTotal}`
|
|
);
|
|
Assert.ok(
|
|
["B", "KB", "MB"].includes(extractedDeltaUnit),
|
|
`Delta unit is reasonable: ${extractedDeltaUnit}`
|
|
);
|
|
|
|
// Now check that the conversion was meaningful.
|
|
// Let's just check that the number displayed is within 10% of `delta`.
|
|
let computedDelta =
|
|
getMemoryMultiplier(extractedDeltaUnit, extractedDeltaSign) *
|
|
deltaTotalNumber;
|
|
Assert.equal(
|
|
computedDelta >= 0,
|
|
delta >= 0,
|
|
`Delta has the right sign: ${computedDelta} vs ${delta}`
|
|
);
|
|
}
|
|
|
|
function extractProcessDetails(row) {
|
|
let children = row.children;
|
|
let name = children[0];
|
|
let memory = children[1];
|
|
let cpu = children[2];
|
|
if (Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons")) {
|
|
name = name.firstChild;
|
|
Assert.ok(
|
|
name.nextSibling.classList.contains("profiler-icon"),
|
|
"The profiler icon should be shown"
|
|
);
|
|
}
|
|
let fluentArgs = row.ownerDocument.l10n.getAttributes(name).args;
|
|
let threadDetailsRow = row.nextSibling;
|
|
while (threadDetailsRow) {
|
|
if (threadDetailsRow.classList.contains("process")) {
|
|
threadDetailsRow = null;
|
|
break;
|
|
}
|
|
if (threadDetailsRow.classList.contains("thread-summary")) {
|
|
break;
|
|
}
|
|
threadDetailsRow = threadDetailsRow.nextSibling;
|
|
}
|
|
|
|
return {
|
|
memory,
|
|
cpu,
|
|
pidContent: fluentArgs.pid,
|
|
threads: threadDetailsRow,
|
|
};
|
|
}
|
|
|
|
function findTabRowByName(doc, name) {
|
|
for (let row of doc.getElementsByClassName("name")) {
|
|
if (!row.parentNode.classList.contains("window")) {
|
|
continue;
|
|
}
|
|
let foundName = document.l10n.getAttributes(row).args.name;
|
|
if (foundName != name) {
|
|
continue;
|
|
}
|
|
return row.parentNode;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findProcessRowByOrigin(doc, origin) {
|
|
for (let row of doc.getElementsByClassName("process")) {
|
|
if (row.process.origin == origin) {
|
|
return row;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function setupTabWithOriginAndTitle(origin, title) {
|
|
let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true });
|
|
tab.testTitle = title;
|
|
tab.testOrigin = origin;
|
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => {
|
|
content.document.title = title;
|
|
});
|
|
return tab;
|
|
}
|
|
|
|
async function setupAudioTab() {
|
|
let origin = "about:blank";
|
|
let title = "utility audio";
|
|
let tab = BrowserTestUtils.addTab(gBrowser, origin, { skipAnimation: true });
|
|
tab.testTitle = title;
|
|
tab.testOrigin = origin;
|
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [title], async title => {
|
|
content.document.title = title;
|
|
const ROOT =
|
|
"https://example.com/browser/toolkit/components/aboutprocesses/tests/browser";
|
|
let audio = content.document.createElement("audio");
|
|
audio.setAttribute("controls", "true");
|
|
audio.setAttribute("loop", true);
|
|
audio.src = `${ROOT}/small-shot.mp3`;
|
|
content.document.body.appendChild(audio);
|
|
await audio.play();
|
|
});
|
|
return tab;
|
|
}
|
|
|
|
async function testAboutProcessesWithConfig({ showAllFrames, showThreads }) {
|
|
const isFission = gFissionBrowser;
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
["toolkit.aboutProcesses.showAllSubframes", showAllFrames],
|
|
["toolkit.aboutProcesses.showThreads", showThreads],
|
|
// Force same-origin tabs to share a single process, to properly test
|
|
// functionality involving multiple tabs within a single process with Fission.
|
|
["dom.ipc.processCount.webIsolated", 1],
|
|
// Ensure utility audio decoder is enabled
|
|
["media.utility-process.enabled", true],
|
|
],
|
|
});
|
|
|
|
// Install a test extension to also cover processes and sub-frames related to the
|
|
// extension process.
|
|
const extension = ExtensionTestUtils.loadExtension({
|
|
manifest: {
|
|
browser_specific_settings: {
|
|
gecko: { id: "test-aboutprocesses@mochi.test" },
|
|
},
|
|
},
|
|
background() {
|
|
// Creates an about:blank iframe in the extension process to make sure that
|
|
// Bug 1665099 doesn't regress.
|
|
document.body.appendChild(document.createElement("iframe"));
|
|
|
|
this.browser.test.sendMessage("bg-page-loaded");
|
|
},
|
|
});
|
|
|
|
await extension.startup();
|
|
await extension.awaitMessage("bg-page-loaded");
|
|
|
|
// Setup tabs asynchronously.
|
|
|
|
// The about:processes tab.
|
|
info("Setting up about:processes");
|
|
let promiseTabAboutProcesses = BrowserTestUtils.openNewForegroundTab({
|
|
gBrowser,
|
|
opening: "about:processes",
|
|
waitForLoad: true,
|
|
});
|
|
|
|
info("Setting up example.com");
|
|
// Another tab that we'll pretend is hung.
|
|
let promiseTabHung = (async function () {
|
|
let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", {
|
|
skipAnimation: true,
|
|
});
|
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
|
let p = BrowserTestUtils.browserLoaded(
|
|
tab.linkedBrowser,
|
|
true /* includeSubFrames */
|
|
);
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
|
|
// Open an in-process iframe to test toolkit.aboutProcesses.showAllSubframes
|
|
let frame = content.document.createElement("iframe");
|
|
content.document.body.appendChild(frame);
|
|
});
|
|
await p;
|
|
return tab;
|
|
})();
|
|
|
|
let promiseAudioPlayback = setupAudioTab();
|
|
|
|
let promiseUserContextTab = (async function () {
|
|
let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com", {
|
|
userContextId: 1,
|
|
skipAnimation: true,
|
|
});
|
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
|
|
content.document.title = "Tab with User Context";
|
|
});
|
|
return tab;
|
|
})();
|
|
|
|
info("Setting up tabs we intend to close");
|
|
|
|
// The two following tabs share the same domain.
|
|
// We use them to check that closing one doesn't close the other.
|
|
let promiseTabCloseSeparately1 = setupTabWithOriginAndTitle(
|
|
"http://example.org",
|
|
"Close me 1 (separately)"
|
|
);
|
|
let promiseTabCloseSeparately2 = setupTabWithOriginAndTitle(
|
|
"http://example.org",
|
|
"Close me 2 (separately)"
|
|
);
|
|
|
|
// The two following tabs share the same domain.
|
|
// We use them to check that closing the process kills them both.
|
|
let promiseTabCloseProcess1 = setupTabWithOriginAndTitle(
|
|
"http://example.net",
|
|
"Close me 1 (process)"
|
|
);
|
|
|
|
let promiseTabCloseProcess2 = setupTabWithOriginAndTitle(
|
|
"http://example.net",
|
|
"Close me 2 (process)"
|
|
);
|
|
|
|
// The two following tabs share the same domain.
|
|
// We use them to check that closing the process kills them both.
|
|
let promiseTabCloseTogether1 = setupTabWithOriginAndTitle(
|
|
"https://example.org",
|
|
"Close me 1 (together)"
|
|
);
|
|
|
|
let promiseTabCloseTogether2 = setupTabWithOriginAndTitle(
|
|
"https://example.org",
|
|
"Close me 2 (together)"
|
|
);
|
|
|
|
// Wait for initialization to finish.
|
|
let tabAboutProcesses = await promiseTabAboutProcesses;
|
|
let tabHung = await promiseTabHung;
|
|
let audioPlayback = await promiseAudioPlayback;
|
|
let tabUserContext = await promiseUserContextTab;
|
|
let tabCloseSeparately1 = await promiseTabCloseSeparately1;
|
|
let tabCloseSeparately2 = await promiseTabCloseSeparately2;
|
|
let tabCloseProcess1 = await promiseTabCloseProcess1;
|
|
let tabCloseProcess2 = await promiseTabCloseProcess2;
|
|
let tabCloseTogether1 = await promiseTabCloseTogether1;
|
|
let tabCloseTogether2 = await promiseTabCloseTogether2;
|
|
|
|
let doc = tabAboutProcesses.linkedBrowser.contentDocument;
|
|
let tbody = doc.getElementById("process-tbody");
|
|
Assert.ok(!!tbody, "Found the #process-tbody element");
|
|
|
|
if (isFission) {
|
|
// We're going to kill this process later, so tell it to add an
|
|
// annotation so the leak checker knows it is okay there is no
|
|
// leak log.
|
|
await SpecialPowers.spawn(tabCloseProcess1.linkedBrowser, [], () => {
|
|
ChromeUtils.privateNoteIntentionalCrash();
|
|
});
|
|
}
|
|
|
|
info("Setting up fake process hang detector");
|
|
let hungChildID = tabHung.linkedBrowser.frameLoader.childID;
|
|
|
|
// Keep informing about:processes that `tabHung` is hung.
|
|
// Note: this is a background task, do not `await` it.
|
|
let fakeProcessHangMonitor = async function () {
|
|
for (let i = 0; i < 100; ++i) {
|
|
if (!tabHung.linkedBrowser) {
|
|
// Let's stop spamming as soon as we can.
|
|
return;
|
|
}
|
|
|
|
Services.obs.notifyObservers(
|
|
{
|
|
childID: hungChildID,
|
|
scriptBrowser: tabHung.linkedBrowser,
|
|
scriptFileName: "chrome://browser/content/browser.js",
|
|
QueryInterface: ChromeUtils.generateQI(["nsIHangReport"]),
|
|
},
|
|
"process-hang-report"
|
|
);
|
|
|
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
}
|
|
};
|
|
fakeProcessHangMonitor();
|
|
|
|
// about:processes will take a little time to appear and be populated.
|
|
await promiseAboutProcessesUpdated({ doc, tabAboutProcesses });
|
|
Assert.ok(tbody.childElementCount, "The table should be populated");
|
|
Assert.ok(
|
|
!!tbody.getElementsByClassName("hung").length,
|
|
"The hung process should appear"
|
|
);
|
|
|
|
info("Looking at the contents of about:processes");
|
|
let processesToBeFound = [
|
|
// The browser process.
|
|
{
|
|
name: "browser",
|
|
predicate: row => row.process.type == "browser",
|
|
},
|
|
// The hung process.
|
|
{
|
|
name: "hung",
|
|
predicate: row =>
|
|
row.classList.contains("hung") &&
|
|
row.classList.contains("process") &&
|
|
["web", "webIsolated"].includes(row.process.type),
|
|
},
|
|
// Any non-hung process
|
|
{
|
|
name: "non-hung",
|
|
predicate: row =>
|
|
!row.classList.contains("hung") &&
|
|
row.classList.contains("process") &&
|
|
["web", "webIsolated"].includes(row.process.type),
|
|
},
|
|
// A utility process with at least one actor.
|
|
{
|
|
name: "utility",
|
|
predicate: row =>
|
|
row.process &&
|
|
row.process.type == "utility" &&
|
|
row.classList.contains("process") &&
|
|
row.nextSibling &&
|
|
row.nextSibling.classList.contains("actor"),
|
|
},
|
|
];
|
|
for (let finder of processesToBeFound) {
|
|
info(`Running sanity tests on ${finder.name}`);
|
|
let row = tbody.firstChild;
|
|
while (row) {
|
|
if (finder.predicate(row)) {
|
|
break;
|
|
}
|
|
row = row.nextSibling;
|
|
}
|
|
Assert.ok(!!row, `found a table row for ${finder.name}`);
|
|
let { memory, cpu, pidContent, threads } = extractProcessDetails(row);
|
|
|
|
info("Sanity checks: pid");
|
|
let pid = Number.parseInt(pidContent);
|
|
Assert.ok(pid > 0, `Checking pid ${pidContent}`);
|
|
Assert.equal(pid, row.process.pid);
|
|
|
|
info("Sanity checks: memory resident");
|
|
await testMemory(
|
|
memory,
|
|
row.process.totalRamSize,
|
|
row.process.deltaRamSize,
|
|
HARDCODED_ASSUMPTIONS_PROCESS
|
|
);
|
|
|
|
info("Sanity checks: CPU (Total)");
|
|
await testCpu(
|
|
cpu,
|
|
row.process.totalCpu,
|
|
row.process.slopeCpu,
|
|
HARDCODED_ASSUMPTIONS_PROCESS
|
|
);
|
|
|
|
// Testing threads.
|
|
if (!showThreads) {
|
|
info("In this mode, we shouldn't display any threads");
|
|
Assert.equal(
|
|
threads,
|
|
null,
|
|
"In hidden threads mode, we shouldn't have any thread summary"
|
|
);
|
|
} else {
|
|
Assert.ok(threads, "We have a thread summary row");
|
|
|
|
let {
|
|
number,
|
|
active = 0,
|
|
list,
|
|
} = doc.l10n.getAttributes(threads.children[0].children[1]).args;
|
|
|
|
info("Sanity checks: number of threads");
|
|
Assert.greaterOrEqual(
|
|
number,
|
|
HARDCODED_ASSUMPTIONS_PROCESS.minimalNumberOfThreads
|
|
);
|
|
Assert.lessOrEqual(
|
|
number,
|
|
HARDCODED_ASSUMPTIONS_PROCESS.maximalNumberOfThreads
|
|
);
|
|
Assert.equal(
|
|
number,
|
|
row.process.threads.length,
|
|
"The number we display should be the number of threads"
|
|
);
|
|
|
|
info("Sanity checks: number of active threads");
|
|
Assert.greaterOrEqual(
|
|
active,
|
|
0,
|
|
"The number of active threads should never be negative"
|
|
);
|
|
Assert.lessOrEqual(
|
|
active,
|
|
number,
|
|
"The number of active threads should not exceed the total number of threads"
|
|
);
|
|
let activeThreads = row.process.threads.filter(t => t.active);
|
|
Assert.equal(
|
|
active,
|
|
activeThreads.length,
|
|
"The displayed number of active threads should be correct"
|
|
);
|
|
|
|
let activeSet = new Set();
|
|
for (let t of activeThreads) {
|
|
activeSet.add(t.name.replace(/ ?#[0-9]+$/, ""));
|
|
}
|
|
info("Sanity checks: thread list");
|
|
Assert.equal(
|
|
list ? list.split(", ").length : 0,
|
|
activeSet.size,
|
|
"The thread summary list of active threads should have the expected length"
|
|
);
|
|
|
|
info("Testing that we can open the list of threads");
|
|
let twisty = threads.getElementsByClassName("twisty")[0];
|
|
twisty.click();
|
|
|
|
// Fluent will update the text content of new rows during the
|
|
// next refresh driver tick, wait for it.
|
|
// requestAnimationFrame calls us at the begining of the tick, we use
|
|
// dispatchToMainThread to execute our code after the end of it.
|
|
//XXX: Replace with proper wait for l10n completion once bug 1520659 is fixed.
|
|
await new Promise(doc.defaultView.requestAnimationFrame);
|
|
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
|
|
|
|
let numberOfThreadsFound = 0;
|
|
for (
|
|
let threadRow = threads.nextSibling;
|
|
threadRow && threadRow.classList.contains("thread");
|
|
threadRow = threadRow.nextSibling
|
|
) {
|
|
numberOfThreadsFound++;
|
|
}
|
|
Assert.equal(
|
|
numberOfThreadsFound,
|
|
number,
|
|
`We should see ${number} threads, found ${numberOfThreadsFound}`
|
|
);
|
|
let threadIds = [];
|
|
for (
|
|
let threadRow = threads.nextSibling;
|
|
threadRow && threadRow.classList.contains("thread");
|
|
threadRow = threadRow.nextSibling
|
|
) {
|
|
Assert.ok(
|
|
threadRow.children.length >= 3 && threadRow.children[1].textContent,
|
|
"The thread row should be populated"
|
|
);
|
|
let children = threadRow.children;
|
|
let cpu = children[1];
|
|
let l10nArgs = doc.l10n.getAttributes(children[0]).args;
|
|
|
|
// Sanity checks: name
|
|
Assert.ok(threadRow.thread.name, "Thread name is not empty");
|
|
Assert.equal(
|
|
l10nArgs.name,
|
|
threadRow.thread.name,
|
|
"Displayed thread name is correct"
|
|
);
|
|
|
|
// Sanity checks: tid
|
|
let tidContent = l10nArgs.tid;
|
|
let tid = Number.parseInt(tidContent);
|
|
threadIds.push(tid);
|
|
Assert.notEqual(tid, 0, "The tid should be set");
|
|
Assert.equal(tid, threadRow.thread.tid, "Displayed tid is correct");
|
|
|
|
// Sanity checks: CPU (per thread)
|
|
await testCpu(
|
|
cpu,
|
|
threadRow.thread.totalCpu,
|
|
threadRow.thread.slopeCpu,
|
|
HARDCODED_ASSUMPTIONS_THREAD
|
|
);
|
|
}
|
|
// By default, threads are sorted by tid.
|
|
let threadList = threadIds.join(",");
|
|
Assert.equal(
|
|
threadList,
|
|
threadIds.sort((a, b) => a - b).join(","),
|
|
"The thread rows are in the default sort order."
|
|
);
|
|
}
|
|
}
|
|
|
|
await promiseAboutProcessesUpdated({
|
|
doc,
|
|
force: true,
|
|
tabAboutProcesses,
|
|
});
|
|
|
|
// Testing subframes.
|
|
info("Testing subframes");
|
|
let foundAtLeastOneInProcessSubframe = false;
|
|
for (let row of doc.getElementsByClassName("window")) {
|
|
let subframe = row.win;
|
|
if (subframe.tab) {
|
|
continue;
|
|
}
|
|
let url = doc.l10n.getAttributes(row.children[0]).args.url;
|
|
Assert.equal(url, subframe.documentURI.spec);
|
|
if (!subframe.isProcessRoot) {
|
|
foundAtLeastOneInProcessSubframe = true;
|
|
}
|
|
}
|
|
if (showAllFrames) {
|
|
Assert.ok(
|
|
foundAtLeastOneInProcessSubframe,
|
|
"Found at least one about:blank in-process subframe"
|
|
);
|
|
} else {
|
|
Assert.ok(
|
|
!foundAtLeastOneInProcessSubframe,
|
|
"We shouldn't have any about:blank in-process subframe"
|
|
);
|
|
}
|
|
|
|
info("Double-clicking on a tab");
|
|
let whenTabSwitchedToWeb = BrowserTestUtils.switchTab(gBrowser, () => {
|
|
// We pass a function to use `BrowserTestUtils.switchTab` not in its
|
|
// role as a tab switcher but rather in its role as a function that
|
|
// waits until something else has switched the tab.
|
|
// We'll actually cause tab switching below, by doucle-clicking
|
|
// in `about:processes`.
|
|
});
|
|
await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
|
|
// Locate and double-click on the representation of `tabHung`.
|
|
let tbody = content.document.getElementById("process-tbody");
|
|
for (let row of tbody.getElementsByClassName("tab")) {
|
|
if (row.parentNode.win.documentURI.spec != "http://example.com/") {
|
|
continue;
|
|
}
|
|
// Simulate double-click.
|
|
let evt = new content.window.MouseEvent("dblclick", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: content.window,
|
|
});
|
|
row.dispatchEvent(evt);
|
|
return;
|
|
}
|
|
Assert.ok(false, "We should have found the hung tab");
|
|
});
|
|
|
|
info("Waiting for tab switch");
|
|
await whenTabSwitchedToWeb;
|
|
Assert.equal(
|
|
gBrowser.selectedTab.linkedBrowser.currentURI.spec,
|
|
tabHung.linkedBrowser.currentURI.spec,
|
|
"We should have focused the hung tab"
|
|
);
|
|
|
|
await BrowserTestUtils.switchTab(gBrowser, tabAboutProcesses);
|
|
|
|
info("Double-clicking on the extensions process");
|
|
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
|
|
await SpecialPowers.spawn(tabAboutProcesses.linkedBrowser, [], async () => {
|
|
let extensionsRow =
|
|
content.document.getElementsByClassName("extensions")[0];
|
|
Assert.ok(!!extensionsRow, "We should have found the extensions process");
|
|
let evt = new content.window.MouseEvent("dblclick", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
view: content.window,
|
|
});
|
|
extensionsRow.dispatchEvent(evt);
|
|
});
|
|
info("Waiting for about:addons to open");
|
|
await tabPromise;
|
|
Assert.equal(
|
|
gBrowser.selectedTab.linkedBrowser.currentURI.spec,
|
|
"about:addons",
|
|
"We should now see the addon tab"
|
|
);
|
|
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
|
|
|
info("Testing tab closing");
|
|
|
|
// A list of processes we have killed and for which we're waiting
|
|
// death confirmation. Only used in Fission.
|
|
let waitForProcessesToDisappear = [];
|
|
await promiseAboutProcessesUpdated({
|
|
doc,
|
|
force: true,
|
|
tabAboutProcesses,
|
|
});
|
|
if (isFission) {
|
|
// Before closing, all our origins should be present
|
|
for (let origin of [
|
|
"http://example.com", // tabHung
|
|
"http://example.net", // tabCloseProcess*
|
|
"http://example.org", // tabCloseSeparately*
|
|
"https://example.org", // tabCloseTogether*
|
|
]) {
|
|
Assert.ok(
|
|
findProcessRowByOrigin(doc, origin),
|
|
`There is a process for origin ${origin}`
|
|
);
|
|
}
|
|
|
|
// Verify that the user context id has been correctly displayed.
|
|
let userContextProcessRow = findProcessRowByOrigin(
|
|
doc,
|
|
"http://example.com^userContextId=1"
|
|
);
|
|
Assert.ok(
|
|
userContextProcessRow,
|
|
"There is a separate process for the tab with a different user context"
|
|
);
|
|
let name = userContextProcessRow.firstChild;
|
|
if (
|
|
Services.prefs.getBoolPref("toolkit.aboutProcesses.showProfilerIcons")
|
|
) {
|
|
name = name.firstChild;
|
|
Assert.ok(
|
|
name.nextSibling.classList.contains("profiler-icon"),
|
|
"The profiler icon should be shown"
|
|
);
|
|
}
|
|
Assert.equal(
|
|
doc.l10n.getAttributes(name).args.origin,
|
|
"http://example.com — " +
|
|
ContextualIdentityService.getUserContextLabel(1),
|
|
"The user context ID should be replaced with the localized container name"
|
|
);
|
|
|
|
// These origins will disappear.
|
|
for (let origin of [
|
|
"http://example.net", // tabCloseProcess*
|
|
"https://example.org", // tabCloseTogether*
|
|
]) {
|
|
let row = findProcessRowByOrigin(doc, origin);
|
|
let childID = row.process.childID;
|
|
waitForProcessesToDisappear.push(promiseProcessDied({ childID }));
|
|
}
|
|
}
|
|
|
|
// Close a few tabs.
|
|
for (let tab of [tabCloseSeparately1, tabCloseTogether1, tabCloseTogether2]) {
|
|
info("Closing a tab through about:processes");
|
|
let found = findTabRowByName(doc, tab.linkedBrowser.contentTitle);
|
|
Assert.ok(
|
|
found,
|
|
`We should have found tab ${tab.linkedBrowser.contentTitle} to close it`
|
|
);
|
|
let closeIcons = found.getElementsByClassName("close-icon");
|
|
Assert.equal(
|
|
closeIcons.length,
|
|
1,
|
|
"This tab should have exactly one close icon"
|
|
);
|
|
closeIcons[0].click();
|
|
Assert.ok(
|
|
found.classList.contains("killing"),
|
|
"We should have marked the row as dying"
|
|
);
|
|
}
|
|
|
|
//...and a process, if we're in Fission.
|
|
if (isFission) {
|
|
info("Closing an entire process through about:processes");
|
|
let found = findProcessRowByOrigin(doc, "http://example.net");
|
|
let closeIcons = found.getElementsByClassName("close-icon");
|
|
Assert.equal(
|
|
closeIcons.length,
|
|
1,
|
|
"This process should have exactly one close icon"
|
|
);
|
|
closeIcons[0].click();
|
|
Assert.ok(
|
|
found.classList.contains("killing"),
|
|
"We should have marked the row as dying"
|
|
);
|
|
|
|
info("Closing the audio utility process through about:processes");
|
|
let utilityRow;
|
|
for (let row of doc.getElementsByClassName("process")) {
|
|
if (row.process.type == "utility") {
|
|
utilityRow = row;
|
|
break;
|
|
}
|
|
}
|
|
Assert.ok(utilityRow, "Should have found audio utility row.");
|
|
closeIcons = utilityRow.getElementsByClassName("close-icon");
|
|
Assert.equal(
|
|
closeIcons.length,
|
|
1,
|
|
"The utility process should have exactly one close icon"
|
|
);
|
|
closeIcons[0].click();
|
|
Assert.ok(
|
|
utilityRow.classList.contains("killing"),
|
|
"We should have marked the utility process as dying"
|
|
);
|
|
}
|
|
|
|
// Give Firefox a little time to close the tabs and update about:processes.
|
|
// This might take two updates as we're racing between collecting data and
|
|
// processes actually being killed.
|
|
await promiseAboutProcessesUpdated({
|
|
doc,
|
|
force: true,
|
|
tabAboutProcesses,
|
|
});
|
|
|
|
// The tabs we have closed directly or indirectly should now be (closed or crashed) and invisible in about:processes.
|
|
for (let { origin, tab } of [
|
|
{ origin: "http://example.org", tab: tabCloseSeparately1 },
|
|
{ origin: "https://example.org", tab: tabCloseTogether1 },
|
|
{ origin: "https://example.org", tab: tabCloseTogether2 },
|
|
...(isFission
|
|
? [
|
|
{ origin: "http://example.net", tab: tabCloseProcess1 },
|
|
{ origin: "http://example.net", tab: tabCloseProcess2 },
|
|
]
|
|
: []),
|
|
]) {
|
|
// Tab shouldn't show up anymore in about:processes
|
|
Assert.ok(
|
|
!findTabRowByName(doc, origin),
|
|
`Tab for ${origin} shouldn't show up anymore in about:processes`
|
|
);
|
|
// ...and should be unloaded.
|
|
Assert.ok(
|
|
!tab.getAttribute("linkedPanel"),
|
|
`The tab should now be unloaded (${tab.testOrigin} - ${tab.testTitle})`
|
|
);
|
|
}
|
|
|
|
// On the other hand, tabs we haven't closed should still be open and visible in about:processes.
|
|
Assert.ok(
|
|
tabCloseSeparately2.linkedBrowser,
|
|
"Killing one tab in the domain should not have closed the other tab"
|
|
);
|
|
let foundtabCloseSeparately2 = findTabRowByName(
|
|
doc,
|
|
tabCloseSeparately2.linkedBrowser.contentTitle
|
|
);
|
|
Assert.ok(
|
|
foundtabCloseSeparately2,
|
|
"The second tab is still visible in about:processes"
|
|
);
|
|
|
|
if (isFission) {
|
|
// After closing, we must have closed some of our origins.
|
|
for (let origin of [
|
|
"http://example.com", // tabHung
|
|
"http://example.org", // tabCloseSeparately*
|
|
]) {
|
|
Assert.ok(
|
|
findProcessRowByOrigin(doc, origin),
|
|
`There should still be a process row for origin ${origin}`
|
|
);
|
|
}
|
|
|
|
info("Waiting for processes to die");
|
|
await Promise.all(waitForProcessesToDisappear);
|
|
|
|
info("Waiting for about:processes to be updated");
|
|
await promiseAboutProcessesUpdated({
|
|
doc,
|
|
force: true,
|
|
tabAboutProcesses,
|
|
});
|
|
|
|
for (let origin of [
|
|
"http://example.net", // tabCloseProcess*
|
|
"https://example.org", // tabCloseTogether*
|
|
]) {
|
|
Assert.ok(
|
|
!findProcessRowByOrigin(doc, origin),
|
|
`Process ${origin} should disappear from about:processes`
|
|
);
|
|
}
|
|
}
|
|
|
|
info("Additional sanity check for all processes");
|
|
for (let row of doc.getElementsByClassName("process")) {
|
|
let { pidContent } = extractProcessDetails(row);
|
|
Assert.equal(Number.parseInt(pidContent), row.process.pid);
|
|
}
|
|
BrowserTestUtils.removeTab(tabAboutProcesses);
|
|
BrowserTestUtils.removeTab(tabHung);
|
|
BrowserTestUtils.removeTab(tabUserContext);
|
|
BrowserTestUtils.removeTab(tabCloseSeparately2);
|
|
|
|
// We still need to remove these tabs.
|
|
// We killed the process, but we don't want to leave zombie tabs lying around.
|
|
BrowserTestUtils.removeTab(tabCloseProcess1);
|
|
BrowserTestUtils.removeTab(tabCloseProcess2);
|
|
BrowserTestUtils.removeTab(audioPlayback);
|
|
|
|
await SpecialPowers.popPrefEnv();
|
|
|
|
await extension.unload();
|
|
}
|