From 4e16c9ae1f5829fe7eb42431dd9ccef3f53d883d Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 19 Oct 2023 22:44:03 -0500 Subject: [PATCH] The Gang Goes Overboard --- app/pages/system/CapacityUtilizationPage.tsx | 72 ++++++-------------- app/pages/system/metrics-util.spec.ts | 68 +++++++++++++----- app/pages/system/metrics-util.ts | 44 ++++++++++-- 3 files changed, 107 insertions(+), 77 deletions(-) diff --git a/app/pages/system/CapacityUtilizationPage.tsx b/app/pages/system/CapacityUtilizationPage.tsx index dbab91491..94dffbfeb 100644 --- a/app/pages/system/CapacityUtilizationPage.tsx +++ b/app/pages/system/CapacityUtilizationPage.tsx @@ -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 { @@ -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([ @@ -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 ( @@ -256,13 +224,13 @@ const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => { {mergedResults.map((result) => ( {result.siloName} - {result.metrics.cpusProvisioned} + {result.metrics.cpus_provisioned} - {bytesToTiB(result.metrics.virtualDiskSpaceProvisioned)} + {bytesToTiB(result.metrics.virtual_disk_space_provisioned)} TiB - {bytesToGiB(result.metrics.ramProvisioned)} + {bytesToGiB(result.metrics.ram_provisioned)} GiB @@ -270,4 +238,4 @@ const UsageTab = memo(({ silos }: { silos: SiloResultsPage }) => {
) -}) +} diff --git a/app/pages/system/metrics-util.spec.ts b/app/pages/system/metrics-util.spec.ts index 8596dcf35..276654047 100644 --- a/app/pages/system/metrics-util.spec.ts +++ b/app/pages/system/metrics-util.spec.ts @@ -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, + }, + }, ]) }) diff --git a/app/pages/system/metrics-util.ts b/app/pages/system/metrics-util.ts index fedcd1aa4..941bd106c 100644 --- a/app/pages/system/metrics-util.ts +++ b/app/pages/system/metrics-util.ts @@ -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 + metrics: Record } -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)) } + }) }