Skip to content

Commit

Permalink
The Gang Goes Overboard
Browse files Browse the repository at this point in the history
  • Loading branch information
david-crespo committed Oct 20, 2023
1 parent c31d035 commit 4e16c9a
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 77 deletions.
72 changes: 20 additions & 52 deletions app/pages/system/CapacityUtilizationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { getLocalTimeZone, now } from '@internationalized/date'
import { useIsFetching } from '@tanstack/react-query'
import { memo, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'

import type { SiloResultsPage } from '@oxide/api'
import {
Expand All @@ -28,15 +28,14 @@ import {
Table,
Tabs,
} from '@oxide/ui'
import { bytesToGiB, bytesToTiB, camelCase } from '@oxide/util'
import { bytesToGiB, bytesToTiB } from '@oxide/util'

import { CapacityMetric, capacityQueryParams } from 'app/components/CapacityMetric'
import { useIntervalPicker } from 'app/components/RefetchIntervalPicker'
import { SystemMetric } from 'app/components/SystemMetric'
import { useDateTimeRangePicker } from 'app/components/form'

import type { SiloMetric } from './metrics-util'
import { mergeSiloMetrics } from './metrics-util'
import { tabularizeSiloMetrics } from './metrics-util'

CapacityUtilizationPage.loader = async () => {
await Promise.all([
Expand Down Expand Up @@ -189,54 +188,23 @@ const MetricsTab = ({
)
}

const usageTableParams = {
startTime: new Date(0),
endTime: capacityQueryParams.endTime,
limit: 1,
order: 'descending' as const,
}

const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
const siloList = silos?.items.map((silo) => ({ name: silo.name, id: silo.id })) || []

const results = useApiQueries('systemMetric', [
...siloList.map((silo) => ({
path: { metricName: 'virtual_disk_space_provisioned' as const },
query: { ...usageTableParams, silo: silo.name },
})),
...siloList.map((silo) => ({
path: { metricName: 'ram_provisioned' as const },
query: { ...usageTableParams, silo: silo.name },
})),
...siloList.map((silo) => ({
path: { metricName: 'cpus_provisioned' as const },
query: { ...usageTableParams, silo: silo.name },
})),
])
function UsageTab({ silos }: { silos: SiloResultsPage }) {
const results = useApiQueries(
'systemMetric',
silos.items.flatMap((silo) => {
const query = { ...capacityQueryParams, silo: silo.name }
return [
{ path: { metricName: 'virtual_disk_space_provisioned' as const }, query },
{ path: { metricName: 'ram_provisioned' as const }, query },
{ path: { metricName: 'cpus_provisioned' as const }, query },
]
})
)

// TODO: loading state, this could take some time
if (results.some((result) => result.isPending)) return null

const siloResults = results
.map((result) => {
if (result.data && result.data.params) {
const params = result.data.params

if (!params.query) {
return undefined
}

return {
siloName: params.query.silo,
metrics: {
[camelCase(params.path.metricName)]: result.data.items[0].datum.datum,
},
} as SiloMetric
}
})
.filter((item): item is SiloMetric => Boolean(item))

const mergedResults = mergeSiloMetrics(siloResults)
const mergedResults = tabularizeSiloMetrics(results)

return (
<Table className="w-full">
Expand All @@ -256,18 +224,18 @@ const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
{mergedResults.map((result) => (
<Table.Row key={result.siloName}>
<Table.Cell width="25%">{result.siloName}</Table.Cell>
<Table.Cell width="25%">{result.metrics.cpusProvisioned}</Table.Cell>
<Table.Cell width="25%">{result.metrics.cpus_provisioned}</Table.Cell>
<Table.Cell width="25%">
{bytesToTiB(result.metrics.virtualDiskSpaceProvisioned)}
{bytesToTiB(result.metrics.virtual_disk_space_provisioned)}
<span className="ml-1 inline-block text-quaternary">TiB</span>
</Table.Cell>
<Table.Cell width="25%">
{bytesToGiB(result.metrics.ramProvisioned)}
{bytesToGiB(result.metrics.ram_provisioned)}
<span className="ml-1 inline-block text-quaternary">GiB</span>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
})
}
68 changes: 50 additions & 18 deletions app/pages/system/metrics-util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,59 @@
*/
import { expect, test } from 'vitest'

import { mergeSiloMetrics } from './metrics-util'
import type { SystemMetricName } from '@oxide/api'

test('mergeSiloMetrics', () => {
expect(mergeSiloMetrics([])).toEqual([])
expect(mergeSiloMetrics([{ siloName: 'a', metrics: { m1: 1 } }])).toEqual([
{ siloName: 'a', metrics: { m1: 1 } },
])
expect(
mergeSiloMetrics([
{ siloName: 'a', metrics: { m1: 1 } },
{ siloName: 'a', metrics: { m2: 2 } },
])
).toEqual([{ siloName: 'a', metrics: { m1: 1, m2: 2 } }])
import type { MetricsResult } from './metrics-util'
import { tabularizeSiloMetrics } from './metrics-util'

function makeResult(
silo: string,
metricName: SystemMetricName,
value: number
): MetricsResult {
return {
data: {
items: [
{
datum: { type: 'i64', datum: value },
timestamp: new Date(),
},
],
params: { query: { silo }, path: { metricName } },
},
}
}

test('tabularizeSiloMetrics', () => {
expect(tabularizeSiloMetrics([])).toEqual([])
expect(
mergeSiloMetrics([
{ siloName: 'a', metrics: { m1: 1 } },
{ siloName: 'a', metrics: { m2: 2 } },
{ siloName: 'b', metrics: { m1: 3 } },
tabularizeSiloMetrics([
makeResult('a', 'virtual_disk_space_provisioned', 1),
makeResult('b', 'virtual_disk_space_provisioned', 2),
makeResult('a', 'cpus_provisioned', 3),
makeResult('b', 'cpus_provisioned', 4),
makeResult('a', 'ram_provisioned', 5),
makeResult('b', 'ram_provisioned', 6),
// here to make sure it gets ignored and doesn't break anything
// @ts-expect-error
{ error: 'whoops' },
])
).toEqual([
{ siloName: 'a', metrics: { m1: 1, m2: 2 } },
{ siloName: 'b', metrics: { m1: 3 } },
{
siloName: 'a',
metrics: {
virtual_disk_space_provisioned: 1,
cpus_provisioned: 3,
ram_provisioned: 5,
},
},
{
siloName: 'b',
metrics: {
virtual_disk_space_provisioned: 2,
cpus_provisioned: 4,
ram_provisioned: 6,
},
},
])
})
44 changes: 37 additions & 7 deletions app/pages/system/metrics-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,46 @@
*
* Copyright Oxide Computer Company
*/
import type {
MeasurementResultsPage,
SystemMetricName,
SystemMetricPathParams,
SystemMetricQueryParams,
} from '@oxide/api'
import { groupBy } from '@oxide/util'

export type SiloMetric = {
export type MetricsResult = {
data?: MeasurementResultsPage & {
params: {
path: SystemMetricPathParams
query?: SystemMetricQueryParams
}
}
}

type SiloMetric = {
siloName: string
metrics: Record<string, number>
metrics: Record<SystemMetricName, number>
}

export const mergeSiloMetrics = (results: SiloMetric[]): SiloMetric[] => {
return groupBy(results, (result) => result.siloName).map(([siloName, values]) => ({
siloName,
metrics: Object.assign({}, ...values.map((v) => v.metrics)),
}))
/**
* Turn the big list of query results into something we can display in a table.
* See the tests for an example.
*/
export function tabularizeSiloMetrics(results: MetricsResult[]): SiloMetric[] {
const processed = results
// filter mostly to ensure we don't try to pull data off an error response
.filter((r) => r.data?.params.query?.silo)
.map((r) => {
const metricName = r.data!.params.path.metricName
const value = r.data!.items[0].datum.datum as number
return {
siloName: r.data!.params.query!.silo!,
metrics: { [metricName]: value },
}
})

return groupBy(processed, (r) => r.siloName).map(([siloName, results]) => {
return { siloName, metrics: Object.assign({}, ...results.map((r) => r.metrics)) }
})
}

0 comments on commit 4e16c9a

Please sign in to comment.