| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import '//resources/ash/common/cr_elements/cr_input/cr_input.js'; |
| import '//resources/ash/common/cr_elements/md_select.css.js'; |
| |
| import {sendWithPromise} from '//resources/js/cr.js'; |
| import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import type {HealthdApiProcessInfo, HealthdApiProcessResult} from '../../utils/externs.js'; |
| import type {HealthdInternalsPage} from '../../utils/page_interface.js'; |
| import {UiUpdateHelper} from '../../utils/ui_update_helper.js'; |
| |
| import {getTemplate} from './process.html.js'; |
| |
| /** |
| * The data structure for each row in process table. |
| */ |
| interface DisplayedProcessInfo { |
| // Process ID (PID). |
| processId: number; |
| // Filename of the executable, which will be `<unknown>` if the data from |
| // healthd is null. |
| name: string; |
| // Process priority. |
| priority: number; |
| // Nice value of the process. |
| nice: number; |
| // Process state. |
| state: string; |
| // Number of threads in the process. |
| threadsNumber: number; |
| // User the process is running as. |
| userId: number; |
| // PID of the parent of this process. |
| parentProcessId: number; |
| // Group ID of this process. |
| processGroupId: number; |
| // Amount of resident memory currently used by the process, in KiB. |
| residentMemoryKib: number; |
| // Uptime of the process, in clock ticks. |
| uptimeTicks: number; |
| // Attempted count of read syscalls. |
| readSystemCallsCount: number; |
| // Attempted count of write syscalls. |
| writeSystemCallsCount: number; |
| // Command which started the process. |
| command: string; |
| } |
| |
| /** |
| * The ID of sort column. Note that the value should be the same as the |
| * corresponding field name in `DisplayedProcessInfo`. |
| */ |
| enum SortColumnEnum { |
| PROCESS_ID = 'processId', |
| NAME = 'name', |
| PRIORITY = 'priority', |
| NICE = 'nice', |
| STATE = 'state', |
| THREADS_NUMBER = 'threadsNumber', |
| USER_ID = 'userId', |
| PARENT_ID = 'parentProcessId', |
| GROUP_ID = 'processGroupId', |
| RESIDENT_MEMORY = 'residentMemoryKib', |
| UPTIME = 'uptimeTicks', |
| READ_SYSCALL_COUNT = 'readSystemCallsCount', |
| WRITE_SYSCALL_COUNT = 'writeSystemCallsCount', |
| COMMAND = 'command', |
| } |
| |
| /** |
| * The ID of sort order. |
| */ |
| enum SortOrderEnum { |
| ASCEND = 'ascend', |
| DESCEND = 'descend', |
| } |
| |
| interface ProcessHeader { |
| title: string; |
| sortColumnId: SortColumnEnum; |
| } |
| |
| function filterProcessData(data: HealthdApiProcessInfo[], filterQuery: string): |
| HealthdApiProcessInfo[] { |
| if (filterQuery === '') { |
| return data; |
| } |
| |
| return data.filter((process: HealthdApiProcessInfo) => { |
| const displayedName: string = |
| (process.name === undefined) ? '<unknown>' : process.name; |
| if (displayedName.includes(filterQuery)) { |
| return true; |
| } |
| |
| for (const stringField |
| of [process.processId, process.state, process.threadsNumber, |
| process.parentProcessId, process.processGroupId, |
| process.readSystemCallsCount, process.writeSystemCallsCount, |
| process.command]) { |
| if (stringField.includes(filterQuery)) { |
| return true; |
| } |
| } |
| |
| for (const numberField of [process.nice, process.priority]) { |
| if (numberField.toString().includes(filterQuery)) { |
| return true; |
| } |
| } |
| |
| return false; |
| }); |
| } |
| |
| function convertProcessData(processes: HealthdApiProcessInfo[]): |
| DisplayedProcessInfo[] { |
| return processes.map( |
| (item: HealthdApiProcessInfo) => ({ |
| processId: parseInt(item.processId), |
| name: (item.name === undefined) ? '<unknown>' : item.name, |
| priority: item.priority, |
| nice: item.nice, |
| state: item.state, |
| threadsNumber: parseInt(item.threadsNumber), |
| userId: parseInt(item.userId), |
| parentProcessId: parseInt(item.parentProcessId), |
| processGroupId: parseInt(item.processGroupId), |
| residentMemoryKib: parseInt(item.residentMemoryKib), |
| uptimeTicks: parseInt(item.uptimeTicks), |
| readSystemCallsCount: parseInt(item.readSystemCallsCount), |
| writeSystemCallsCount: parseInt(item.writeSystemCallsCount), |
| command: item.command, |
| })); |
| } |
| |
| function sortProcessData( |
| processes: DisplayedProcessInfo[], sortColumn: SortColumnEnum, |
| sortOrder: SortOrderEnum): DisplayedProcessInfo[] { |
| return processes.sort((a, b) => { |
| if (a[sortColumn] === b[sortColumn]) { |
| return 0; |
| } |
| if (sortOrder === SortOrderEnum.ASCEND) { |
| return (a[sortColumn] < b[sortColumn]) ? -1 : 1; |
| } |
| if (sortOrder === SortOrderEnum.DESCEND) { |
| return (a[sortColumn] < b[sortColumn]) ? 1 : -1; |
| } |
| |
| console.error('Unknown sort order: ', sortOrder); |
| return 0; |
| }); |
| } |
| |
| export interface HealthdInternalsProcessElement { |
| $: { |
| pageSizeSelector: HTMLSelectElement, |
| sortColumnSelector: HTMLSelectElement, |
| sortOrderSelector: HTMLSelectElement, |
| }; |
| } |
| |
| export class HealthdInternalsProcessElement extends PolymerElement implements |
| HealthdInternalsPage { |
| static get is() { |
| return 'healthd-internals-process'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| displayedHeaders: {type: Array}, |
| processData: {type: Array}, |
| filterQuery: {type: String}, |
| pageSize: {type: Number}, |
| sortColumn: {type: String}, |
| sortOrder: {type: String}, |
| displayedData: { |
| type: Array, |
| computed: 'getDisplayedData(processData, ' + |
| 'filterQuery, pageSize, sortColumn, sortOrder)', |
| }, |
| lastUpdateTime: {type: String}, |
| }; |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| // Process data is not used in line charts, so we only need to use the same |
| // UI update frequency to fetch process data here and update process page. |
| this.updateHelper = new UiUpdateHelper(() => { |
| sendWithPromise('getHealthdProcessInfo') |
| .then((data: HealthdApiProcessResult) => { |
| this.processData = data.processes; |
| this.lastUpdateTime = new Date().toLocaleTimeString(); |
| }); |
| }); |
| } |
| |
| // The title and column ID for headers and selected menu in process table. |
| private readonly displayedHeaders: ProcessHeader[] = [ |
| {title: 'Process ID', sortColumnId: SortColumnEnum.PROCESS_ID}, |
| {title: 'Name', sortColumnId: SortColumnEnum.NAME}, |
| {title: 'Priority', sortColumnId: SortColumnEnum.PRIORITY}, |
| {title: 'Nice', sortColumnId: SortColumnEnum.NICE}, |
| {title: 'Process State', sortColumnId: SortColumnEnum.STATE}, |
| {title: 'Threads Number', sortColumnId: SortColumnEnum.THREADS_NUMBER}, |
| {title: 'User ID', sortColumnId: SortColumnEnum.USER_ID}, |
| {title: 'Parent ID', sortColumnId: SortColumnEnum.PARENT_ID}, |
| {title: 'Group ID', sortColumnId: SortColumnEnum.GROUP_ID}, |
| { |
| title: 'Resident Memory (KiB)', |
| sortColumnId: SortColumnEnum.RESIDENT_MEMORY, |
| }, |
| {title: 'Uptime Ticks', sortColumnId: SortColumnEnum.UPTIME}, |
| { |
| title: 'Count of read syscall', |
| sortColumnId: SortColumnEnum.READ_SYSCALL_COUNT, |
| }, |
| { |
| title: 'Count of write syscall', |
| sortColumnId: SortColumnEnum.WRITE_SYSCALL_COUNT, |
| }, |
| {title: 'Command', sortColumnId: SortColumnEnum.COMMAND}, |
| ]; |
| |
| // Latest process data from healthd. |
| private processData: HealthdApiProcessInfo[] = []; |
| |
| // The user entered filter query. |
| private filterQuery: string = ''; |
| |
| // The target column for sorting. |
| private sortColumn: SortColumnEnum = SortColumnEnum.PROCESS_ID; |
| |
| // Sorting order. |
| private sortOrder: SortOrderEnum = SortOrderEnum.ASCEND; |
| |
| // Number of processes displayed in the table. |
| private pageSize: number = 100; |
| |
| // Data displayed in the process table. |
| private displayedData: DisplayedProcessInfo[] = []; |
| |
| // The time that the process data is last updated. |
| private lastUpdateTime: string = ''; |
| |
| // Helper for updating UI regularly. Init in `connectedCallback`. |
| private updateHelper: UiUpdateHelper; |
| |
| updateVisibility(isVisible: boolean) { |
| this.updateHelper.updateVisibility(isVisible); |
| } |
| |
| updateUiUpdateInterval(intervalSeconds: number) { |
| this.updateHelper.updateUiUpdateInterval(intervalSeconds); |
| } |
| |
| private getDisplayedData( |
| data: HealthdApiProcessInfo[], filterQuery: string, pageSize: number, |
| sortColumn: SortColumnEnum, |
| sortOrder: SortOrderEnum): DisplayedProcessInfo[] { |
| return sortProcessData( |
| convertProcessData(filterProcessData(data, filterQuery)), |
| sortColumn, sortOrder) |
| .slice(0, pageSize); |
| } |
| |
| private onPageSizeChanged() { |
| this.pageSize = parseInt(this.$.pageSizeSelector.value); |
| } |
| |
| private onSortColumnChanged() { |
| this.sortColumn = this.$.sortColumnSelector.value as SortColumnEnum; |
| } |
| |
| private onSortOrderChanged() { |
| this.sortOrder = this.$.sortOrderSelector.value as SortOrderEnum; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'healthd-internals-process': HealthdInternalsProcessElement; |
| } |
| } |
| |
| customElements.define( |
| HealthdInternalsProcessElement.is, HealthdInternalsProcessElement); |