Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
"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 =
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(
"Close me 1 (separately)"
);
let promiseTabCloseSeparately2 = setupTabWithOriginAndTitle(
"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(
"Close me 1 (process)"
);
let promiseTabCloseProcess2 = setupTabWithOriginAndTitle(
"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(
"Close me 1 (together)"
);
let promiseTabCloseTogether2 = setupTabWithOriginAndTitle(
"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,
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,
);
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"
);
}
// 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();
}