chromium/chrome/browser/resources/chromeos/healthd_internals/pages/process.ts

// 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 {HealthdApiProcessInfo, HealthdApiProcessResult} from '../externs.js';

import {getTemplate} from './process.html.js';
import {HealthdInternalsPage} from './utils/page_interface.js';
import {UiUpdateHelper} from './utils/ui_update_helper.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 {
  $: {
    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},
      sortColumn: {type: String},
      sortOrder: {type: String},
      displayedData: {
        type: Array,
        computed:
            'getDisplayedData(processData, filterQuery, 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;

  // 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,
      sortColumn: SortColumnEnum,
      sortOrder: SortOrderEnum): DisplayedProcessInfo[] {
    return sortProcessData(
        convertProcessData(filterProcessData(data, filterQuery)), sortColumn,
        sortOrder);
  }

  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);