Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/components/TimeSeriesChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type TimeSeriesChartProps = {
startTime: Date
endTime: Date
unit?: string
yAxisTickFormatter?: (val: number) => string
}

const TICK_COUNT = 6
Expand All @@ -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
Expand Down Expand Up @@ -182,7 +184,7 @@ export default function TimeSeriesChart({
orientation="right"
tick={textMonoMd}
tickMargin={8}
tickFormatter={(val) => val.toLocaleString()}
tickFormatter={yAxisTickFormatter}
padding={{ top: 32 }}
{...yTicks}
/>
Expand Down
28 changes: 28 additions & 0 deletions app/pages/project/instances/instance/tabs/MetricsTab.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
86 changes: 73 additions & 13 deletions app/pages/project/instances/instance/tabs/MetricsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,27 +64,77 @@ 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])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the last value not necessarily being the largest by looking at all values and finding the max. I always feel weird about spreading 3000 args into Math.max but I tested it a while ago (we do this elsewhere) and it is apparently fine.


// 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 (
<div className="flex w-1/2 flex-grow flex-col">
<h2 className="ml-3 flex items-center text-mono-xs text-secondary">
{title} {unit && <div className="ml-1 text-quaternary">{unit}</div>}
<h2 className="ml-3 flex items-center text-mono-xs text-secondary ">
{title} <div className="ml-1 normal-case text-quaternary">{label}</div>
{isLoading && <Spinner className="ml-2" />}
</h2>
<Suspense fallback={<div className="mt-3 h-[300px]" />}>
<TimeSeriesChart
className="mt-3"
data={data}
title={title}
unit={unitForSet}
width={480}
height={240}
startTime={startTime}
endTime={endTime}
yAxisTickFormatter={yAxisTickFormatter}
/>
</Suspense>
</div>
Expand Down Expand Up @@ -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 */}
<div className="flex w-full space-x-4">
<DiskMetric {...commonProps} title="Reads" unit="(Count)" metric="read" />
<DiskMetric {...commonProps} title="Read" unit="(Bytes)" metric="read_bytes" />
<DiskMetric {...commonProps} title="Reads" unit="Count" metric="read" />
<DiskMetric {...commonProps} title="Read" unit="Bytes" metric="read_bytes" />
</div>

<div className="flex w-full space-x-4">
<DiskMetric {...commonProps} title="Writes" unit="(Count)" metric="write" />
<DiskMetric {...commonProps} title="Write" unit="(Bytes)" metric="write_bytes" />
<DiskMetric {...commonProps} title="Writes" unit="Count" metric="write" />
<DiskMetric {...commonProps} title="Write" unit="Bytes" metric="write_bytes" />
</div>

<div className="flex w-full space-x-4">
<DiskMetric {...commonProps} title="Flushes" unit="(Count)" metric="flush" />
<DiskMetric {...commonProps} title="Flushes" unit="Count" metric="flush" />
</div>
</div>
</>
Expand Down