// 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 {assert} from '//resources/js/assert.js';
import {sendWithPromise} from '//resources/js/cr.js';
import {LINE_CHART_COLOR_SET} from './constants.js';
import {CpuUsageHelper} from './cpu_usage_helper.js';
import type {CpuUsage} from './cpu_usage_helper.js';
import type {HealthdApiBatteryResult, HealthdApiCpuResult, HealthdApiMemoryResult, HealthdApiTelemetryResult, HealthdApiThermalResult} from './externs.js';
import {DataSeries} from './line_chart/utils/data_series.js';
import type {HealthdInternalsGenericChartElement} from './pages/generic_chart.js';
import type {HealthdInternalsTelemetryElement} from './pages/telemetry.js';
const LINE_CHART_BATTERY_HEADERS: string[] = [
'Voltage (V)',
'Charge (Ah)',
'Current (A)',
];
const LINE_CHART_MEMORY_HEADERS: string[] = [
'Available',
'Free',
'Buffers',
'Page Cache',
'Shared',
'Active',
'Inactive',
'Total Slab',
'Reclaim Slab',
'Unreclaim Slab',
];
function getLineChartColor(index: number) {
const colorIdx: number = index % LINE_CHART_COLOR_SET.length;
return LINE_CHART_COLOR_SET[colorIdx];
}
function sortThermals(
first: HealthdApiThermalResult, second: HealthdApiThermalResult): number {
if (first.source === second.source) {
return first.name.localeCompare(second.name);
}
return first.source.localeCompare(second.source);
}
export interface LineChartPages {
battery: HealthdInternalsGenericChartElement;
cpuFrequency: HealthdInternalsGenericChartElement;
cpuUsage: HealthdInternalsGenericChartElement;
memory: HealthdInternalsGenericChartElement;
thermal: HealthdInternalsGenericChartElement;
}
/**
* Helper class to collect and maintain the displayed data.
*/
export class DataManager {
constructor(
dataRetentionDuration: number,
telemetryPage: HealthdInternalsTelemetryElement,
chartPages: LineChartPages) {
this.dataRetentionDuration = dataRetentionDuration;
this.telemetryPage = telemetryPage;
this.chartPages = chartPages;
this.initBatteryDataSeries();
this.initMemoryDataSeries();
}
// Historical data for line chart. The following `DataSeries` collection
// is fixed and initialized in constructor.
// - Battery data.
private batteryDataSeries: DataSeries[] = [];
// - Memory data.
private memoryDataSeries: DataSeries[] = [];
// Historical data for line chart. The following `DataSeries` collection
// is dynamic and initialized when the first batch of data is obtained.
// - Frequency data of logical CPUs.
private cpuFrequencyDataSeries: DataSeries[] = [];
// - CPU usage of logical CPUs in percentage.
private cpuUsageDataSeries: DataSeries[] = [];
// - Temperature data of thermal sensors.
private thermalDataSeries: DataSeries[] = [];
// Set in constructor.
private readonly telemetryPage: HealthdInternalsTelemetryElement;
private readonly chartPages: LineChartPages;
// The helper class for calculating CPU usage.
private readonly cpuUsageHelper: CpuUsageHelper = new CpuUsageHelper();
// The data fetching interval ID used for cancelling the running interval.
private fetchDataInternalId?: number = undefined;
// The duration (in milliseconds) that the data will be retained.
private dataRetentionDuration: number;
/**
* Set up periodic fetch data requests to the backend to get required info.
*
* @param pollingCycle - Polling cycle in milliseconds.
*/
setupFetchDataRequests(pollingCycle: number) {
if (this.fetchDataInternalId !== undefined) {
clearInterval(this.fetchDataInternalId);
this.fetchDataInternalId = undefined;
}
const fetchData = () => {
sendWithPromise('getHealthdTelemetryInfo')
.then((data: HealthdApiTelemetryResult) => {
this.handleHealthdTelemetryInfo(data);
});
};
fetchData();
this.fetchDataInternalId = setInterval(fetchData, pollingCycle);
}
updateDataRetentionDuration(durationHours: number) {
this.dataRetentionDuration = durationHours * 60 * 60 * 1000;
this.removeOutdatedData(Date.now());
}
private handleHealthdTelemetryInfo(data: HealthdApiTelemetryResult) {
data.thermals.sort(sortThermals);
this.telemetryPage.updateTelemetryData(data);
const timestamp: number = Date.now();
if (data.battery !== undefined) {
this.updateBatteryData(data.battery, timestamp);
}
this.updateCpuFrequencyData(data.cpu, timestamp);
this.updateMemoryData(data.memory, timestamp);
this.updateThermalData(data.thermals, timestamp);
const cpuUsage: CpuUsage[][]|null =
this.cpuUsageHelper.getCpuUsage(data.cpu);
if (cpuUsage !== null) {
this.updateCpuUsageData(cpuUsage, timestamp);
}
this.removeOutdatedData(timestamp);
}
private removeOutdatedData(endTime: number) {
const newStartTime = endTime - this.dataRetentionDuration;
const shouldUpdateChart = (dataSeriesList: DataSeries[]) => {
return dataSeriesList.reduce(
// The order within `or` expression is important because
// `removeOutdatedData` should be called for each `dataSeries`.
(isDataRemoved, dataSeries) =>
dataSeries.removeOutdatedData(newStartTime) || isDataRemoved,
false);
};
if (shouldUpdateChart(this.batteryDataSeries)) {
this.chartPages.battery.updateStartTime(newStartTime);
}
if (shouldUpdateChart(this.cpuFrequencyDataSeries)) {
this.chartPages.cpuFrequency.updateStartTime(newStartTime);
}
if (shouldUpdateChart(this.cpuUsageDataSeries)) {
this.chartPages.cpuUsage.updateStartTime(newStartTime);
}
if (shouldUpdateChart(this.memoryDataSeries)) {
this.chartPages.memory.updateStartTime(newStartTime);
}
if (shouldUpdateChart(this.thermalDataSeries)) {
this.chartPages.thermal.updateStartTime(newStartTime);
}
}
private updateBatteryData(
battery: HealthdApiBatteryResult, timestamp: number) {
this.batteryDataSeries[0].addDataPoint(battery.voltageNow, timestamp);
this.batteryDataSeries[1].addDataPoint(battery.chargeNow, timestamp);
this.batteryDataSeries[2].addDataPoint(battery.currentNow, timestamp);
}
private updateCpuFrequencyData(cpu: HealthdApiCpuResult, timestamp: number) {
if (this.cpuFrequencyDataSeries.length === 0) {
this.initCpuFrequencyDataSeries(cpu);
}
const cpuNumber: number = cpu.physicalCpus.reduce(
(acc, item) => acc + item.logicalCpus.length, 0);
if (cpuNumber !== this.cpuFrequencyDataSeries.length) {
console.warn('CPU frequency data: Number of CPUs changed.');
return;
}
let count: number = 0;
for (const physicalCpu of cpu.physicalCpus) {
for (const logicalCpu of physicalCpu.logicalCpus) {
this.cpuFrequencyDataSeries[count].addDataPoint(
parseInt(logicalCpu.frequency.current), timestamp);
count += 1;
}
}
}
private updateCpuUsageData(
physcialCpuUsage: CpuUsage[][], timestamp: number) {
if (this.cpuUsageDataSeries.length === 0) {
this.initCpuUsageDataSeries(physcialCpuUsage);
}
const cpuNumber: number =
physcialCpuUsage.reduce((acc, item) => acc + item.length, 0);
if (cpuNumber !== this.cpuUsageDataSeries.length - 1) {
console.warn('CPU usage data: Number of CPUs changed.');
return;
}
if (cpuNumber === 0) {
console.warn('CPU usage data: CPU not found.');
return;
}
let sumCpuUsage: number = 0;
let count: number = 1;
for (const logicalCpuUsage of physcialCpuUsage) {
for (const cpuUsage of logicalCpuUsage) {
if (cpuUsage.usagePercentage !== null) {
this.cpuUsageDataSeries[count].addDataPoint(
cpuUsage.usagePercentage, timestamp);
sumCpuUsage += cpuUsage.usagePercentage;
}
count += 1;
}
}
this.cpuUsageDataSeries[0].addDataPoint(sumCpuUsage / cpuNumber, timestamp);
}
private updateMemoryData(memory: HealthdApiMemoryResult, timestamp: number) {
const itemsInChart: Array<string|undefined> = [
memory.availableMemoryKib,
memory.freeMemoryKib,
memory.buffersKib,
memory.pageCacheKib,
memory.sharedMemoryKib,
memory.activeMemoryKib,
memory.inactiveMemoryKib,
memory.totalSlabMemoryKib,
memory.reclaimableSlabMemoryKib,
memory.unreclaimableSlabMemoryKib,
];
assert(itemsInChart.length === this.memoryDataSeries.length);
for (const [index, item] of itemsInChart.entries()) {
if (item !== undefined) {
this.memoryDataSeries[index].addDataPoint(parseInt(item), timestamp);
}
}
}
private updateThermalData(
thermals: HealthdApiThermalResult[], timestamp: number) {
if (this.thermalDataSeries.length === 0) {
this.initThermalDataSeries(thermals);
}
if (thermals.length !== this.thermalDataSeries.length) {
console.warn('Thermal data: Number of thermal sensors changed.');
return;
}
for (const [index, thermal] of thermals.entries()) {
this.thermalDataSeries[index].addDataPoint(
thermal.temperatureCelsius, timestamp);
}
}
private initBatteryDataSeries() {
for (const [index, header] of LINE_CHART_BATTERY_HEADERS.entries()) {
this.batteryDataSeries.push(
new DataSeries(header, getLineChartColor(index)));
}
this.chartPages.battery.addDataSeries(this.batteryDataSeries);
}
private initCpuFrequencyDataSeries(cpu: HealthdApiCpuResult) {
let count: number = 0;
for (const [physicalCpuId, physicalCpu] of cpu.physicalCpus.entries()) {
for (let logicalCpuId: number = 0;
logicalCpuId < physicalCpu.logicalCpus.length; ++logicalCpuId) {
this.cpuFrequencyDataSeries.push(new DataSeries(
`CPU #${physicalCpuId}-${logicalCpuId}`, getLineChartColor(count)));
count += 1;
}
}
this.chartPages.cpuFrequency.addDataSeries(this.cpuFrequencyDataSeries);
}
private initCpuUsageDataSeries(physcialCpuUsage: CpuUsage[][]) {
this.cpuUsageDataSeries.push(
new DataSeries('Overall', getLineChartColor(0)));
let count: number = 1;
for (const [physicalCpuId, logicalCpuUsage] of physcialCpuUsage.entries()) {
for (let logicalCpuId: number = 0; logicalCpuId < logicalCpuUsage.length;
++logicalCpuId) {
this.cpuUsageDataSeries.push(new DataSeries(
`CPU #${physicalCpuId}-${logicalCpuId}`, getLineChartColor(count)));
count += 1;
}
}
this.chartPages.cpuUsage.addDataSeries(this.cpuUsageDataSeries);
}
private initMemoryDataSeries() {
for (const [index, header] of LINE_CHART_MEMORY_HEADERS.entries()) {
this.memoryDataSeries.push(
new DataSeries(header, getLineChartColor(index)));
}
this.chartPages.memory.addDataSeries(this.memoryDataSeries);
}
private initThermalDataSeries(thermals: HealthdApiThermalResult[]) {
for (const [index, thermal] of thermals.entries()) {
this.thermalDataSeries.push(new DataSeries(
`${thermal.name} (${thermal.source})`, getLineChartColor(index)));
}
this.chartPages.thermal.addDataSeries(this.thermalDataSeries);
}
}