diff --git a/app/components/TimeSeriesChart.tsx b/app/components/TimeSeriesChart.tsx index 026267cfc0..70dc91f933 100644 --- a/app/components/TimeSeriesChart.tsx +++ b/app/components/TimeSeriesChart.tsx @@ -110,6 +110,7 @@ type TimeSeriesChartProps = { startTime: Date endTime: Date unit?: string + yAxisTickFormatter?: (val: number) => string } const TICK_COUNT = 6 @@ -129,6 +130,7 @@ export default function TimeSeriesChart({ startTime, endTime, unit, + yAxisTickFormatter = (val) => val.toLocaleString(), }: TimeSeriesChartProps) { // We use the largest data point +20% for the graph scale. !rawData doesn't // mean it's empty (it will never be empty because we fill in artificial 0s at @@ -182,7 +184,7 @@ export default function TimeSeriesChart({ orientation="right" tick={textMonoMd} tickMargin={8} - tickFormatter={(val) => val.toLocaleString()} + tickFormatter={yAxisTickFormatter} padding={{ top: 32 }} {...yTicks} /> diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.spec.ts b/app/pages/project/instances/instance/tabs/MetricsTab.spec.ts new file mode 100644 index 0000000000..220c63258e --- /dev/null +++ b/app/pages/project/instances/instance/tabs/MetricsTab.spec.ts @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { expect, test } from 'vitest' + +import { getCycleCount } from './MetricsTab' + +test('getCycleCount', () => { + expect(getCycleCount(5, 1000)).toEqual(0) + expect(getCycleCount(1000, 1000)).toEqual(0) + expect(getCycleCount(1001, 1000)).toEqual(1) + expect(getCycleCount(10 ** 6, 1000)).toEqual(1) + expect(getCycleCount(10 ** 6 + 1, 1000)).toEqual(2) + expect(getCycleCount(10 ** 9, 1000)).toEqual(2) + expect(getCycleCount(10 ** 9 + 1, 1000)).toEqual(3) + + expect(getCycleCount(5, 1024)).toEqual(0) + expect(getCycleCount(1024, 1024)).toEqual(0) + expect(getCycleCount(1025, 1024)).toEqual(1) + expect(getCycleCount(2 ** 20, 1024)).toEqual(1) + expect(getCycleCount(2 ** 20 + 1, 1024)).toEqual(2) + expect(getCycleCount(2 ** 30, 1024)).toEqual(2) + expect(getCycleCount(2 ** 30 + 1, 1024)).toEqual(3) +}) diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index 6724f4e9c4..5d39592881 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -22,9 +22,19 @@ import { getInstanceSelector, useInstanceSelector } from 'app/hooks' const TimeSeriesChart = React.lazy(() => import('app/components/TimeSeriesChart')) +export function getCycleCount(num: number, base: number) { + let cycleCount = 0 + let transformedValue = num + while (transformedValue > base) { + transformedValue = transformedValue / base + cycleCount++ + } + return cycleCount +} + type DiskMetricParams = { title: string - unit?: string + unit: 'Bytes' | 'Count' startTime: Date endTime: Date metric: DiskMetricName @@ -54,16 +64,64 @@ function DiskMetric({ { placeholderData: (x) => x } ) - const data = (metrics?.items || []).map(({ datum, timestamp }) => ({ - timestamp: timestamp.getTime(), - // all of these metrics are cumulative ints - value: (datum.datum as Cumulativeint64).value, - })) + const isBytesChart = unit === 'Bytes' + + const largestValue = useMemo(() => { + if (!metrics || metrics.items.length === 0) return 0 + return Math.max(...metrics.items.map((m) => (m.datum.datum as Cumulativeint64).value)) + }, [metrics]) + + // We'll need to divide each number in the set by a consistent exponent + // of 1024 (for Bytes) or 1000 (for Counts) + const base = isBytesChart ? 1024 : 1000 + // Figure out what that exponent is: + const cycleCount = getCycleCount(largestValue, base) + + // Now that we know how many cycles of "divide by 1024 || 1000" to run through + // (via cycleCount), we can determine the proper unit for the set + let unitForSet = '' + let label = '(COUNT)' + if (isBytesChart) { + const byteUnits = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'] + unitForSet = byteUnits[cycleCount] + label = `(${unitForSet})` + } + + const divisor = base ** cycleCount + + const data = useMemo( + () => + (metrics?.items || []).map(({ datum, timestamp }) => ({ + timestamp: timestamp.getTime(), + // All of these metrics are cumulative ints. + // The value passed in is what will render in the tooltip. + value: isBytesChart + ? // We pass a pre-divided value to the chart if the unit is Bytes + (datum.datum as Cumulativeint64).value / divisor + : // If the unit is Count, we pass the raw value + (datum.datum as Cumulativeint64).value, + })), + [metrics, isBytesChart, divisor] + ) + + // Create a label for the y-axis ticks. "Count" charts will be + // abbreviated and will have a suffix (e.g. "k") appended. Because + // "Bytes" charts will have already been divided by the divisor + // before the yAxis is created, we can use their given value. + const yAxisTickFormatter = (val: number) => { + if (isBytesChart) { + return val.toLocaleString() + } + const tickValue = (val / divisor).toFixed(2) + const countUnits = ['', 'k', 'M', 'B', 'T'] + const unitForTick = countUnits[cycleCount] + return `${tickValue}${unitForTick}` + } return (
-

- {title} {unit &&
{unit}
} +

+ {title}
{label}
{isLoading && }

}> @@ -71,10 +129,12 @@ function DiskMetric({ className="mt-3" data={data} title={title} + unit={unitForSet} width={480} height={240} startTime={startTime} endTime={endTime} + yAxisTickFormatter={yAxisTickFormatter} />
@@ -151,17 +211,17 @@ export function MetricsTab() { {/* see the following link for the source of truth on what these mean https://github.com/oxidecomputer/crucible/blob/258f162b/upstairs/src/stats.rs#L9-L50 */}
- - + +
- - + +
- +