| // 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 liveProcessesCountTotal = |
| document.querySelector<HTMLElement>('#live-processes-count-total'); |
| assert(liveProcessesCountTotal); |
| liveProcessesCountTotal.innerText = |
| String(info.liveRendererProcessesCountTotal); |
| |
| 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'; |
| } |
| // TODO(crbug.com/398265332): only show the non-default case. |
| if (frame.siteInstance.areJavascriptOptimizersEnabled) { |
| itemLabel += ', js-opt-on'; |
| } else { |
| itemLabel += ', js-opt-off'; |
| } |
| 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); |
| }); |