Skip to content

Commit 5cf4339

Browse files
Dynamically render units on metrics tab (#1916)
* Dynamically render units on metrics tab; improvements to Count and Bytes charts --------- Co-authored-by: David Crespo <[email protected]>
1 parent a26f7c1 commit 5cf4339

File tree

3 files changed

+104
-14
lines changed

3 files changed

+104
-14
lines changed

app/components/TimeSeriesChart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type TimeSeriesChartProps = {
110110
startTime: Date
111111
endTime: Date
112112
unit?: string
113+
yAxisTickFormatter?: (val: number) => string
113114
}
114115

115116
const TICK_COUNT = 6
@@ -129,6 +130,7 @@ export default function TimeSeriesChart({
129130
startTime,
130131
endTime,
131132
unit,
133+
yAxisTickFormatter = (val) => val.toLocaleString(),
132134
}: TimeSeriesChartProps) {
133135
// We use the largest data point +20% for the graph scale. !rawData doesn't
134136
// mean it's empty (it will never be empty because we fill in artificial 0s at
@@ -182,7 +184,7 @@ export default function TimeSeriesChart({
182184
orientation="right"
183185
tick={textMonoMd}
184186
tickMargin={8}
185-
tickFormatter={(val) => val.toLocaleString()}
187+
tickFormatter={yAxisTickFormatter}
186188
padding={{ top: 32 }}
187189
{...yTicks}
188190
/>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
import { expect, test } from 'vitest'
9+
10+
import { getCycleCount } from './MetricsTab'
11+
12+
test('getCycleCount', () => {
13+
expect(getCycleCount(5, 1000)).toEqual(0)
14+
expect(getCycleCount(1000, 1000)).toEqual(0)
15+
expect(getCycleCount(1001, 1000)).toEqual(1)
16+
expect(getCycleCount(10 ** 6, 1000)).toEqual(1)
17+
expect(getCycleCount(10 ** 6 + 1, 1000)).toEqual(2)
18+
expect(getCycleCount(10 ** 9, 1000)).toEqual(2)
19+
expect(getCycleCount(10 ** 9 + 1, 1000)).toEqual(3)
20+
21+
expect(getCycleCount(5, 1024)).toEqual(0)
22+
expect(getCycleCount(1024, 1024)).toEqual(0)
23+
expect(getCycleCount(1025, 1024)).toEqual(1)
24+
expect(getCycleCount(2 ** 20, 1024)).toEqual(1)
25+
expect(getCycleCount(2 ** 20 + 1, 1024)).toEqual(2)
26+
expect(getCycleCount(2 ** 30, 1024)).toEqual(2)
27+
expect(getCycleCount(2 ** 30 + 1, 1024)).toEqual(3)
28+
})

app/pages/project/instances/instance/tabs/MetricsTab.tsx

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,19 @@ import { getInstanceSelector, useInstanceSelector } from 'app/hooks'
2222

2323
const TimeSeriesChart = React.lazy(() => import('app/components/TimeSeriesChart'))
2424

25+
export function getCycleCount(num: number, base: number) {
26+
let cycleCount = 0
27+
let transformedValue = num
28+
while (transformedValue > base) {
29+
transformedValue = transformedValue / base
30+
cycleCount++
31+
}
32+
return cycleCount
33+
}
34+
2535
type DiskMetricParams = {
2636
title: string
27-
unit?: string
37+
unit: 'Bytes' | 'Count'
2838
startTime: Date
2939
endTime: Date
3040
metric: DiskMetricName
@@ -54,27 +64,77 @@ function DiskMetric({
5464
{ placeholderData: (x) => x }
5565
)
5666

57-
const data = (metrics?.items || []).map(({ datum, timestamp }) => ({
58-
timestamp: timestamp.getTime(),
59-
// all of these metrics are cumulative ints
60-
value: (datum.datum as Cumulativeint64).value,
61-
}))
67+
const isBytesChart = unit === 'Bytes'
68+
69+
const largestValue = useMemo(() => {
70+
if (!metrics || metrics.items.length === 0) return 0
71+
return Math.max(...metrics.items.map((m) => (m.datum.datum as Cumulativeint64).value))
72+
}, [metrics])
73+
74+
// We'll need to divide each number in the set by a consistent exponent
75+
// of 1024 (for Bytes) or 1000 (for Counts)
76+
const base = isBytesChart ? 1024 : 1000
77+
// Figure out what that exponent is:
78+
const cycleCount = getCycleCount(largestValue, base)
79+
80+
// Now that we know how many cycles of "divide by 1024 || 1000" to run through
81+
// (via cycleCount), we can determine the proper unit for the set
82+
let unitForSet = ''
83+
let label = '(COUNT)'
84+
if (isBytesChart) {
85+
const byteUnits = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']
86+
unitForSet = byteUnits[cycleCount]
87+
label = `(${unitForSet})`
88+
}
89+
90+
const divisor = base ** cycleCount
91+
92+
const data = useMemo(
93+
() =>
94+
(metrics?.items || []).map(({ datum, timestamp }) => ({
95+
timestamp: timestamp.getTime(),
96+
// All of these metrics are cumulative ints.
97+
// The value passed in is what will render in the tooltip.
98+
value: isBytesChart
99+
? // We pass a pre-divided value to the chart if the unit is Bytes
100+
(datum.datum as Cumulativeint64).value / divisor
101+
: // If the unit is Count, we pass the raw value
102+
(datum.datum as Cumulativeint64).value,
103+
})),
104+
[metrics, isBytesChart, divisor]
105+
)
106+
107+
// Create a label for the y-axis ticks. "Count" charts will be
108+
// abbreviated and will have a suffix (e.g. "k") appended. Because
109+
// "Bytes" charts will have already been divided by the divisor
110+
// before the yAxis is created, we can use their given value.
111+
const yAxisTickFormatter = (val: number) => {
112+
if (isBytesChart) {
113+
return val.toLocaleString()
114+
}
115+
const tickValue = (val / divisor).toFixed(2)
116+
const countUnits = ['', 'k', 'M', 'B', 'T']
117+
const unitForTick = countUnits[cycleCount]
118+
return `${tickValue}${unitForTick}`
119+
}
62120

63121
return (
64122
<div className="flex w-1/2 flex-grow flex-col">
65-
<h2 className="ml-3 flex items-center text-mono-xs text-secondary">
66-
{title} {unit && <div className="ml-1 text-quaternary">{unit}</div>}
123+
<h2 className="ml-3 flex items-center text-mono-xs text-secondary ">
124+
{title} <div className="ml-1 normal-case text-quaternary">{label}</div>
67125
{isLoading && <Spinner className="ml-2" />}
68126
</h2>
69127
<Suspense fallback={<div className="mt-3 h-[300px]" />}>
70128
<TimeSeriesChart
71129
className="mt-3"
72130
data={data}
73131
title={title}
132+
unit={unitForSet}
74133
width={480}
75134
height={240}
76135
startTime={startTime}
77136
endTime={endTime}
137+
yAxisTickFormatter={yAxisTickFormatter}
78138
/>
79139
</Suspense>
80140
</div>
@@ -151,17 +211,17 @@ export function MetricsTab() {
151211
{/* see the following link for the source of truth on what these mean
152212
https://github.com/oxidecomputer/crucible/blob/258f162b/upstairs/src/stats.rs#L9-L50 */}
153213
<div className="flex w-full space-x-4">
154-
<DiskMetric {...commonProps} title="Reads" unit="(Count)" metric="read" />
155-
<DiskMetric {...commonProps} title="Read" unit="(Bytes)" metric="read_bytes" />
214+
<DiskMetric {...commonProps} title="Reads" unit="Count" metric="read" />
215+
<DiskMetric {...commonProps} title="Read" unit="Bytes" metric="read_bytes" />
156216
</div>
157217

158218
<div className="flex w-full space-x-4">
159-
<DiskMetric {...commonProps} title="Writes" unit="(Count)" metric="write" />
160-
<DiskMetric {...commonProps} title="Write" unit="(Bytes)" metric="write_bytes" />
219+
<DiskMetric {...commonProps} title="Writes" unit="Count" metric="write" />
220+
<DiskMetric {...commonProps} title="Write" unit="Bytes" metric="write_bytes" />
161221
</div>
162222

163223
<div className="flex w-full space-x-4">
164-
<DiskMetric {...commonProps} title="Flushes" unit="(Count)" metric="flush" />
224+
<DiskMetric {...commonProps} title="Flushes" unit="Count" metric="flush" />
165225
</div>
166226
</div>
167227
</>

0 commit comments

Comments
 (0)