blob: e30d5baa6828436b705a63cfdbfeb1ed9c131876 [file]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Unit conversion and human-readable formatting for GPU metric values.
//
// Provides helpers that convert raw metric values (nanoseconds, bytes, Hz, etc.)
// into appropriately scaled units (e.g. 1 500 000 000 ns → 1.5 s) while
// preserving the metric's row shape so the rest of the rendering pipeline
// can stay generic.
//
// Also exposes unit-family classification ({@link unitInfo}) and cross-unit
// conversion ({@link convertUnit}) used by the baseline comparison feature.
import type {Terminology} from './terminology';
// Shape of a single metric row fed into the humanization pipeline.
type RowShape = {
metric_id: string | null;
metric_label: string;
metric_unit: string;
metric_value: number | string | null;
};
// Shape of a metric table (description + array of rows).
type TableShape = {table_desc: string | null; data: RowShape[]};
// Shape of a metric section (title + array of tables).
type SectionShape = {section: string; tables: TableShape[]};
// Type guard: returns `true` when `v` is a finite number.
function isNumber(v: unknown): v is number {
return typeof v === 'number' && Number.isFinite(v);
}
// Picks the most readable time unit for a value given in seconds.
// Returns the scaled value and the target unit string.
export function adjustSeconds(value: number): {unit: string; value: number} {
if (value > 1) {
return {unit: 'second', value: value};
}
if (value * 1_000 > 1) {
return {unit: 'msecond', value: value * 1_000};
}
if (value * 1_000_000 > 1) {
return {unit: 'usecond', value: value * 1_000_000};
}
return {unit: 'nsecond', value: value * 1_000_000_000};
}
// Picks the most readable SI prefix for a plain count.
// Returns the scaled value and the prefix character.
function adjustCount(value: number): {
prefix: '' | 'K' | 'M' | 'G';
value: number;
} {
if (value > 1_000_000_000) {
return {prefix: 'G', value: value / 1_000_000_000};
}
if (value > 1_000_000) {
return {prefix: 'M', value: value / 1_000_000};
}
if (value > 1_000) {
return {prefix: 'K', value: value / 1_000};
}
return {prefix: '', value: value};
}
// Picks the most readable binary-prefix unit for a byte value.
// Uses IEC powers of 1024 (KiB, MiB, GiB).
function adjustBytes(value: number): {
unit: 'byte' | 'Kbyte' | 'Mbyte' | 'Gbyte';
value: number;
} {
if (value > 1_073_741_824) {
return {unit: 'Gbyte', value: value / 1_073_741_824};
}
if (value > 1_048_576) {
return {unit: 'Mbyte', value: value / 1_048_576};
}
if (value > 1_024) {
return {unit: 'Kbyte', value: value / 1_024};
}
return {unit: 'byte', value: value};
}
// Humanizes a single metric row by converting units to human-readable form.
export function humanizeRow<T extends RowShape>(
row: T,
terminology?: Terminology,
): T {
const {metric_unit: unit, metric_value: value} = row;
// Leaving strings, nulls, undefined and % rows the same
if (typeof value === 'string' || value == null || unit.includes('%')) {
return row;
}
let humanizedUnit = unit;
let humanizedValue: number | string | null = value;
if (!isNumber(value)) {
return row;
}
// Use current terminology's per-group denominator (defaults to 'block')
const perGroupDenom = terminology?.block.name ?? 'block';
switch (unit) {
// Adjust seconds
case 'second': {
const adj = adjustSeconds(value);
humanizedUnit = adj.unit;
humanizedValue = adj.value;
break;
}
case 'nsecond': {
const adj = adjustSeconds(value / 1_000_000_000);
humanizedUnit = adj.unit;
humanizedValue = adj.value;
break;
}
case 'cycle/second': {
if (value === 0) {
break;
}
const adj = adjustSeconds(1 / value);
humanizedUnit = `cycle/${adj.unit}`;
humanizedValue = 1 / adj.value;
break;
}
// Adjust count
case 'hz': {
const adj = adjustCount(value);
humanizedUnit = `${adj.prefix}hz`;
humanizedValue = adj.value;
break;
}
case 'element/second': {
const adj = adjustCount(value);
humanizedUnit = `${adj.prefix}element/second`;
humanizedValue = adj.value;
break;
}
// Adjust bytes
case 'byte': {
const adj = adjustBytes(value);
humanizedUnit = adj.unit;
humanizedValue = adj.value;
break;
}
case `byte/${perGroupDenom}`: {
const adj = adjustBytes(value);
humanizedUnit = `${adj.unit}/${perGroupDenom}`;
humanizedValue = adj.value;
break;
}
case 'byte/second': {
const adj = adjustBytes(value);
humanizedUnit = `${adj.unit}/second`;
humanizedValue = adj.value;
break;
}
default:
break;
}
return {
// We copy the row and override the metric unit and value with the humanized version
...row,
metric_unit: humanizedUnit,
metric_value: humanizedValue,
};
}
// Humanizes an array of metric sections immutably.
export function humanizeSections<Type extends SectionShape>(
sections: readonly Type[],
terminology?: Terminology,
): Type[] {
// shallow-copying the section
// shallow-copying each table in `tables`
// replacing each row in `data` with its humanized version
const humanizeSection = (section: Type): Type => ({
...section,
// Replacing `tables` with a new array where each table is also shallow-copied and its `data` rows are humanized
tables: section.tables.map((table) => ({
...table,
data: table.data.map((row) => humanizeRow(row, terminology)),
})),
});
// Humanizing each section
return sections.map(humanizeSection);
}
// Family unit conversions used to match baseline metrics to the current selection.
export type UnitFamily =
| 'second'
| 'hz'
| 'byte'
| 'byte/second'
| 'byte/block'
| 'element/second'
| 'cycle'
| 'block'
| 'percent'
| 'other';
// Returns the unit family and relative scaling factor from the base unit.
export function unitInfo(
unit: string,
terminology?: Terminology,
): [UnitFamily, number] {
// percent
if (unit.includes('%')) return ['percent', 1];
// seconds
if (unit === 'second') return ['second', 1];
if (unit === 'msecond') return ['second', 1e-3];
if (unit === 'usecond') return ['second', 1e-6];
if (unit === 'nsecond') return ['second', 1e-9];
// hz / cycle per second
if (unit === 'hz' || unit === 'cycle/second') return ['hz', 1];
if (unit === 'Khz') return ['hz', 1e3];
if (unit === 'Mhz') return ['hz', 1e6];
if (unit === 'Ghz') return ['hz', 1e9];
// bytes
if (unit === 'byte') return ['byte', 1];
if (unit === 'Kbyte') return ['byte', 1024];
if (unit === 'Mbyte') return ['byte', 1024 * 1024];
if (unit === 'Gbyte') return ['byte', 1024 * 1024 * 1024];
// bytes per second
if (unit === 'byte/second') return ['byte/second', 1];
if (unit === 'Kbyte/second') return ['byte/second', 1024];
if (unit === 'Mbyte/second') return ['byte/second', 1024 * 1024];
if (unit === 'Gbyte/second') return ['byte/second', 1024 * 1024 * 1024];
// bytes per block (per-group denominator from terminology)
const perGroupDenom = terminology?.block.name ?? 'block';
if (unit === `byte/${perGroupDenom}`) return ['byte/block', 1];
if (unit === `Kbyte/${perGroupDenom}`) return ['byte/block', 1024];
if (unit === `Mbyte/${perGroupDenom}`) {
return ['byte/block', 1024 * 1024];
}
if (unit === `Gbyte/${perGroupDenom}`) {
return ['byte/block', 1024 * 1024 * 1024];
}
// elements per second
if (unit === 'element/second') return ['element/second', 1];
if (unit === 'Kelement/second') return ['element/second', 1e3];
if (unit === 'Melement/second') return ['element/second', 1e6];
if (unit === 'Gelement/second') return ['element/second', 1e9];
return ['other', 1];
}
// Converts a value from one unit to another within the same unit family.
export function convertUnit(
value: number,
fromUnit: string,
toUnit: string,
terminology?: Terminology,
): number | null {
const [fromFamily, fromFactor] = unitInfo(fromUnit, terminology);
const [toFamily, toFactor] = unitInfo(toUnit, terminology);
// Unknown families: only comparable if unit strings match exactly
if (fromFamily === 'other' || toFamily === 'other') {
return fromUnit === toUnit ? value : null;
}
// Known families: must match, apply scaling when needed
if (fromFamily !== toFamily) return null;
// Percent: no scaling
if (fromFamily === 'percent') return value;
return (value * fromFactor) / toFactor;
}