// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_tree/cr_tree.js';
import type {CrTreeElement} from 'chrome://resources/cr_elements/cr_tree/cr_tree.js';
import type {CrTreeItemElement} from 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js';
import {MAY_HAVE_CHILDREN_ATTR} from 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js';
import {assert} from 'chrome://resources/js/assert.js';
import type {FrameInfo, ProcessInternalsHandlerRemote, WebContentsInfo} from './process_internals.mojom-webui.js';
import {FrameInfo_Type, ProcessInternalsHandler} from './process_internals.mojom-webui.js';
/**
* Reference to the backend providing all the data.
*/
let pageHandler: ProcessInternalsHandlerRemote|null = null;
/**
* @return True if successful.
*/
function selectTab(id: string): boolean {
const tabContents = document.querySelectorAll<HTMLElement>('#content > div');
const navigation = document.querySelector<HTMLElement>('#navigation');
assert(navigation);
const tabHeaders = navigation.querySelectorAll<HTMLElement>('.tab-header');
let found = false;
for (let i = 0; i < tabContents.length; i++) {
const tabContent = tabContents[i]!;
const tabHeader = tabHeaders[i]!;
const isTargetTab = tabContent.id === id;
found = found || isTargetTab;
tabContent.classList.toggle('selected', isTargetTab);
tabHeader.classList.toggle('selected', isTargetTab);
}
if (!found) {
return false;
}
window.location.hash = id;
return true;
}
function onHashChange() {
const hash = window.location.hash.slice(1).toLowerCase();
if (!selectTab(hash)) {
selectTab('general');
}
}
function setupTabs() {
const tabContents = document.querySelectorAll('#content > div');
for (const tabContent of tabContents) {
const contentHeader =
tabContent.querySelector<HTMLElement>('.content-header');
assert(contentHeader);
const tabName = contentHeader.textContent;
const tabHeader = document.createElement('div');
tabHeader.className = 'tab-header';
const button = document.createElement('button');
button.textContent = tabName;
tabHeader.appendChild(button);
tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
const navigation = document.querySelector('#navigation');
assert(navigation);
navigation.appendChild(tabHeader);
}
onHashChange();
}
/**
* Collects and displays info about the renderer process count and limit across
* all profiles.
*/
async function loadProcessCountInfo() {
assert(pageHandler);
const {info} = await pageHandler.getProcessCountInfo();
const processCountTotal =
document.querySelector<HTMLElement>('#process-count-total');
assert(processCountTotal);
processCountTotal.innerText = String(info.rendererProcessCountTotal);
const processCountForLimit =
document.querySelector<HTMLElement>('#process-count-for-limit');
assert(processCountForLimit);
processCountForLimit.innerText = String(info.rendererProcessCountForLimit);
const processLimit = document.querySelector<HTMLElement>('#process-limit');
assert(processLimit);
processLimit.innerText = String(info.rendererProcessLimit);
const overProcessLimit =
document.querySelector<HTMLElement>('#over-process-limit');
assert(overProcessLimit);
overProcessLimit.innerText =
(info.rendererProcessCountForLimit >= info.rendererProcessLimit) ? 'Yes' :
'No';
}
/**
* Root of the WebContents tree.
*/
let treeViewRoot: CrTreeElement|null = null;
/**
* Accumulators for tracking frame and process counts. Reset in
* loadWebContentsInfo.
*/
let totalFrameCount: number = 0;
let totalCrossProcessFrameCount: number = 0;
let processIdSet: Set<number> = new Set();
/**
* Initialize and return |treeViewRoot|.
*/
function getTreeViewRoot(): CrTreeElement {
if (!treeViewRoot) {
treeViewRoot = document.querySelector('cr-tree');
assert(treeViewRoot);
treeViewRoot.detail = {payload: {}, children: {}};
}
return treeViewRoot;
}
/**
* Initialize and return a tree item representing a FrameInfo object and
* recursively creates its subframe objects. For subframes, pass the parent
* frame's process ID in `parentProcessId`.
*/
function frameToTreeItem(frame: FrameInfo, parentProcessId: number = -1):
{item: CrTreeItemElement, count: number} {
// Count out-of-process iframes.
const isCrossProcessFrame: boolean =
parentProcessId !== -1 && parentProcessId !== frame.processId;
if (isCrossProcessFrame) {
totalCrossProcessFrameCount++;
}
processIdSet.add(frame.processId);
// Compose the string which will appear in the entry for this frame.
const prefix = isCrossProcessFrame ? 'OOPIF' : 'Frame';
let itemLabel = `${prefix}[${frame.processId}:${frame.routingId}]:`;
if (frame.type === FrameInfo_Type.kBackForwardCache) {
itemLabel += ` bfcached`;
} else if (frame.type === FrameInfo_Type.kPrerender) {
itemLabel += ` prerender`;
}
itemLabel += ` SI:${frame.siteInstance.id}`;
itemLabel += `, SIG:${frame.siteInstance.siteInstanceGroupId}`;
itemLabel += `, BI:${frame.siteInstance.browsingInstanceId}`;
if (frame.siteInstance.locked) {
itemLabel += ', locked';
} else {
itemLabel += ', unlocked';
}
if (frame.siteInstance.siteUrl) {
itemLabel += `, site:${frame.siteInstance.siteUrl.url}`;
}
if (frame.siteInstance.processLockUrl) {
itemLabel += `, lock:${frame.siteInstance.processLockUrl.url}`;
}
if (frame.siteInstance.requiresOriginKeyedProcess) {
itemLabel += ', origin-keyed';
}
if (frame.siteInstance.isSandboxForIframes) {
itemLabel += ', iframe-sandbox';
}
if (frame.siteInstance.isGuest) {
itemLabel += ', guest';
}
if (frame.siteInstance.isPdf) {
itemLabel += ', pdf';
}
if (frame.siteInstance.storagePartition) {
itemLabel += `, partition:${frame.siteInstance.storagePartition}`;
}
if (frame.lastCommittedUrl) {
itemLabel += ` | url: ${frame.lastCommittedUrl.url}`;
}
const item = document.createElement('cr-tree-item');
item.label = itemLabel;
item.detail = {payload: {}, children: {}};
item.toggleAttribute(MAY_HAVE_CHILDREN_ATTR, true);
item.expanded = true;
let frameCount = 1;
for (const subframe of frame.subframes) {
const result = frameToTreeItem(subframe, frame.processId);
const subItem = result.item;
const count = result.count;
frameCount += count;
item.add(subItem);
}
return {item: item, count: frameCount};
}
/**
* Initialize and return a tree item representing the WebContentsInfo object
* and contains all frames in it as a subtree.
*/
function webContentsToTreeItem(webContents: WebContentsInfo):
CrTreeItemElement {
let itemLabel = 'WebContents: ';
if (webContents.title.length > 0) {
itemLabel += webContents.title + ', ';
}
const item = document.createElement('cr-tree-item');
item.label = itemLabel;
item.detail = {payload: {}, children: {}};
item.toggleAttribute(MAY_HAVE_CHILDREN_ATTR, true);
item.expanded = true;
const result = frameToTreeItem(webContents.rootFrame);
const rootItem = result.item;
const activeCount = result.count;
item.add(rootItem);
// Add data for all root nodes retrieved from back-forward cache.
let cachedCount = 0;
for (const cachedRoot of webContents.bfcachedRootFrames) {
const cachedResult = frameToTreeItem(cachedRoot);
item.add(cachedResult.item);
cachedCount++;
}
// Add data for all root nodes in prerendered pages.
let prerenderCount = 0;
for (const cachedRoot of webContents.prerenderRootFrames) {
const cachedResult = frameToTreeItem(cachedRoot);
item.add(cachedResult.item);
prerenderCount++;
}
// Builds a string according to English pluralization rules:
// buildCountString(0, 'frame') => "0 frames"
// buildCountString(1, 'frame') => "1 frame"
// buildCountString(2, 'frame') => "2 frames"
const buildCountString = ((count: number, name: string) => {
return `${count} ${name}` + (count !== 1 ? 's' : '');
});
itemLabel += buildCountString(activeCount, 'active frame');
if (cachedCount > 0) {
itemLabel += ', ' + buildCountString(cachedCount, 'bfcached root');
}
if (prerenderCount > 0) {
itemLabel += ', ' + buildCountString(prerenderCount, 'prerender root');
}
item.label = itemLabel;
totalFrameCount += activeCount + cachedCount + prerenderCount;
return item;
}
/**
* This is a callback which is invoked when the data for WebContents
* associated with the browser profile is received from the browser process.
*/
function populateWebContentsTab(infos: WebContentsInfo[]) {
const tree = getTreeViewRoot();
// Clear the tree first before populating it with the new content.
tree.items.forEach(item => tree.removeTreeItem(item));
for (const webContents of infos) {
const item = webContentsToTreeItem(webContents);
tree.add(item);
}
}
/**
* Function which retrieves the data for all WebContents associated with the
* current browser profile. The result is passed to populateWebContentsTab.
*/
async function loadWebContentsInfo() {
// Reset frame counts.
totalFrameCount = 0;
totalCrossProcessFrameCount = 0;
processIdSet = new Set();
assert(pageHandler);
const {infos} = await pageHandler.getAllWebContentsInfo();
populateWebContentsTab(infos);
// Post tab, frame, and process counts.
const tabCount = document.querySelector<HTMLElement>('#tab-count');
assert(tabCount);
tabCount.innerText = String(infos.length);
const frameCount = document.querySelector<HTMLElement>('#frame-count');
assert(frameCount);
frameCount.innerText = String(totalFrameCount);
const oopifCount = document.querySelector<HTMLElement>('#oopif-count');
assert(oopifCount);
oopifCount.innerText = String(totalCrossProcessFrameCount);
const processCount =
document.querySelector<HTMLElement>('#profile-process-count');
assert(processCount);
processCount.innerText = String(processIdSet.size);
}
/**
* Function which retrieves the currently active isolated origins and inserts
* them into the page. It organizes these origins into two lists: persisted
* isolated origins, which are triggered by password entry and apply only
* within the current profile, and global isolated origins, which apply to all
* profiles.
*/
function loadIsolatedOriginInfo() {
assert(pageHandler);
// Retrieve any persistent isolated origins for the current profile. Insert
// them into a list on the page if there is at least one such origin.
pageHandler.getUserTriggeredIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
const userOrigins =
document.querySelector<HTMLElement>('#user-triggered-isolated-origins');
assert(userOrigins);
userOrigins.textContent =
'The following origins are isolated because you previously typed a ' +
'password or logged in on these sites (' + originCount + ' total). ' +
'Clear cookies or history to wipe this list; this takes effect ' +
'after a restart.';
const list = document.createElement('ul');
for (const origin of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = origin;
list.appendChild(item);
}
userOrigins.appendChild(list);
});
pageHandler.getWebTriggeredIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
const webOrigins =
document.querySelector<HTMLElement>('#web-triggered-isolated-origins');
assert(webOrigins);
webOrigins.textContent =
'The following origins are isolated based on runtime heuristics ' +
'triggered directly by web pages, such as Cross-Origin-Opener-Policy ' +
'headers. Clear cookies or history to wipe this list; this takes ' +
'effect after a restart.';
const list = document.createElement('ul');
for (const origin of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = origin;
list.appendChild(item);
}
webOrigins.appendChild(list);
});
// Retrieve global isolated origins and insert them into a separate list if
// there is at least one such origin. Since these origins may come from
// multiple sources, include the source info for each origin in parens.
pageHandler.getGloballyIsolatedOrigins().then((response) => {
const originCount = response.isolatedOrigins.length;
if (!originCount) {
return;
}
const globalOrigins =
document.querySelector<HTMLElement>('#global-isolated-origins');
assert(globalOrigins);
globalOrigins.textContent =
'The following origins are isolated by default for all users (' +
originCount + ' total). A description of how each origin was ' +
' activated is provided in parentheses.';
const list = document.createElement('ul');
for (const originInfo of response.isolatedOrigins) {
const item = document.createElement('li');
item.textContent = `${originInfo.origin} (${originInfo.source})`;
list.appendChild(item);
}
globalOrigins.appendChild(list);
});
}
document.addEventListener('DOMContentLoaded', function() {
// Set up Mojo interface to the backend.
pageHandler = ProcessInternalsHandler.getRemote();
assert(pageHandler);
// Set up the tabbed UI.
setupTabs();
// Populate the process count and limit info.
loadProcessCountInfo();
const refreshProcessInfoButton =
document.querySelector<HTMLElement>('#refresh-process-info');
assert(refreshProcessInfoButton);
refreshProcessInfoButton.addEventListener('click', loadProcessCountInfo);
// Get the ProcessPerSite mode and populate it.
pageHandler.getProcessPerSiteMode().then((response) => {
const sharingMode =
document.querySelector<HTMLElement>('#process-per-site-mode');
assert(sharingMode);
sharingMode.innerText = response.mode;
});
// Get the Site Isolation mode and populate it.
pageHandler.getIsolationMode().then((response) => {
const isolationMode =
document.querySelector<HTMLElement>('#isolation-mode');
assert(isolationMode);
isolationMode.innerText = response.mode;
});
loadIsolatedOriginInfo();
// Start loading the information about WebContents.
loadWebContentsInfo();
const refreshFrameTreesButton =
document.querySelector<HTMLElement>('#refresh-frame-trees');
assert(refreshFrameTreesButton);
refreshFrameTreesButton.addEventListener('click', loadWebContentsInfo);
});