| // 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; |
| } |