Skip to content

Commit

Permalink
refacotrs; remove old DiskMetrics; add writes and flushes charts
Browse files Browse the repository at this point in the history
  • Loading branch information
charliepark committed Jan 29, 2025
1 parent b313aac commit 4c61baf
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 128 deletions.
2 changes: 1 addition & 1 deletion app/pages/project/instances/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ export function InstancePage() {
</PropertiesTable.Group>
<RouteTabs fullWidth>
<Tab to={pb.instanceStorage(instanceSelector)}>Storage</Tab>
<Tab to={pb.instanceMetrics(instanceSelector)}>Disk Metrics</Tab>
<Tab to={pb.instanceNetworking(instanceSelector)}>Networking</Tab>
<Tab to={pb.instanceMetrics(instanceSelector)}>Metrics</Tab>
<Tab to={pb.instanceConnect(instanceSelector)}>Connect</Tab>
</RouteTabs>
{resizeInstance && (
Expand Down
155 changes: 28 additions & 127 deletions app/pages/project/instances/instance/tabs/MetricsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import {
useApiQuery,
usePrefetchedApiQuery,
type ChartDatum,
type Cumulativeint64,
type DiskMetricName,
} from '@oxide/api'
import { Storage24Icon } from '@oxide/design-system/icons/react'

Expand All @@ -25,7 +23,6 @@ import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { Listbox } from '~/ui/lib/Listbox'
import { Spinner } from '~/ui/lib/Spinner'
import { TableEmptyBox } from '~/ui/lib/Table'
import { getDurationMinutes } from '~/util/date'

const TimeSeriesChart = React.lazy(() => import('~/components/TimeSeriesChart'))

Expand All @@ -39,6 +36,9 @@ export function getCycleCount(num: number, base: number) {
return cycleCount
}

/** convert to UTC and return the timezone-free format required by OxQL */
const oxqlTimestamp = (date: Date) => date.toISOString().replace('Z', '')

type getOxqlQueryParams = {
metric: string
diskId: string
Expand All @@ -47,18 +47,28 @@ type getOxqlQueryParams = {
}

const getOxqlQuery = ({ metric, diskId, startTime, endTime }: getOxqlQueryParams) => {
const utcStartTime = new Date(startTime.toISOString())
const utcEndTime = new Date(endTime.toISOString())
const duration = getDurationMinutes({ start: utcStartTime, end: utcEndTime })
const query = `get virtual_disk:${metric} | filter timestamp > @now() - ${duration}m && disk_id == "${diskId}" | align mean_within(30s)`
// console.log('query', query)
return query
const start = oxqlTimestamp(startTime)
const end = oxqlTimestamp(endTime)
return `get virtual_disk:${metric} | filter timestamp >= @${start} && timestamp < @${end} && disk_id == "${diskId}" | align mean_within(30s)`
}

// Should probaby be generated in Api.ts
type OxqlDiskMetricName =
| 'bytes_read'
| 'bytes_written'
| 'failed_flushes'
| 'failed_reads'
| 'failed_writes'
| 'flushes'
| 'io_latency'
| 'io_size'
| 'reads'
| 'writes'

type OxqlDiskMetricParams = {
title: string
unit: 'Bytes' | 'Count'
metric: 'reads' | 'bytes_read'
metric: OxqlDiskMetricName
startTime: Date
endTime: Date
diskSelector: {
Expand All @@ -76,7 +86,6 @@ function OxqlDiskMetric({
endTime,
diskSelector,
}: OxqlDiskMetricParams) {
// you need to update the startTime and endTime to account for the user's timezone; should still be a Date
const { diskId } = diskSelector

const query = getOxqlQuery({ metric, diskId, startTime, endTime })
Expand Down Expand Up @@ -164,115 +173,6 @@ function OxqlDiskMetric({
)
}

type DiskMetricParams = {
title: string
unit: 'Bytes' | 'Count'
startTime: Date
endTime: Date
metric: DiskMetricName
diskSelector: {
project: string
disk: string
}
}

function DiskMetric({
title,
unit,
startTime,
endTime,
metric,
diskSelector: { project, disk },
}: DiskMetricParams) {
// TODO: we're only pulling the first page. Should we bump the cap to 10k?
// Fetch multiple pages if 10k is not enough? That's a bit much.
const { data: metrics, isLoading } = useApiQuery(
'diskMetricsList',
{
path: { disk, metric },
query: { project, startTime, endTime, limit: 3000 },
},
// avoid graphs flashing blank while loading when you change the time
{ placeholderData: (x) => x }
)

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 (
<div className="flex w-1/2 grow flex-col">
<h2 className="ml-3 flex items-center text-mono-xs text-default">
{title} <div className="ml-1 normal-case text-tertiary">{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>
)
}

// We could figure out how to prefetch the metrics data, but it would be
// annoying because it relies on the default date range, plus there are 5 calls.
// Considering the data is going to be swapped out as soon as they change the
Expand Down Expand Up @@ -347,23 +247,24 @@ export function Component() {
<div className="mt-8 space-y-12">
{/* 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" />
</div>

<div className="flex w-full space-x-4">
<OxqlDiskMetric {...commonProps} title="Reads" unit="Count" metric="reads" />
<OxqlDiskMetric {...commonProps} title="Read" unit="Bytes" metric="bytes_read" />
</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" />
<OxqlDiskMetric {...commonProps} title="Writes" unit="Count" metric="writes" />
<OxqlDiskMetric
{...commonProps}
title="Write"
unit="Bytes"
metric="bytes_written"
/>
</div>

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

0 comments on commit 4c61baf

Please sign in to comment.