Skip to content

Commit 8b35f7b

Browse files
committed
utils file tweaks, abstract slightly less
1 parent e16b1d7 commit 8b35f7b

File tree

2 files changed

+43
-55
lines changed

2 files changed

+43
-55
lines changed

app/components/oxql-metrics/util.spec.ts

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ import {
1515
getMaxExponent,
1616
getMeanWithinSeconds,
1717
getTimePropsForOxqlQuery,
18-
getTimestamps,
1918
getUnit,
2019
getUtilizationChartProps,
21-
getValuesFromTimeseries,
2220
oxqlTimestamp,
2321
sumValues,
2422
yAxisLabelForCountChart,
@@ -160,10 +158,10 @@ const utilizationQueryResult1: OxqlQueryResult = {
160158
{
161159
values: {
162160
type: 'double',
163-
values: [
164-
4991154550.953981, 5002306111.529594, 5005747970.58788,
165-
4996292893.584528,
166-
],
161+
// there is a bug in the client generator that makes this not allow nulls,
162+
// but we can in fact get them from the API for these values
163+
// @ts-expect-error
164+
values: [4991154550.953981, 5002306111.529594, 5005747970.58788, null],
167165
},
168166
metricType: 'gauge',
169167
},
@@ -177,29 +175,27 @@ const utilizationQueryResult1: OxqlQueryResult = {
177175

178176
const timeseries1 = utilizationQueryResult1.tables[0].timeseries['16671618930358432507']
179177

180-
test('getTimestamps', () => {
181-
const expectedTimestamps = [1740166123000, 1740166183000, 1740166243000, 1740166303000]
182-
expect(getTimestamps(timeseries1)).toEqual(expectedTimestamps)
183-
})
184-
185-
test('getValuesFromTimeseries', () => {
186-
expect(getValuesFromTimeseries(timeseries1)).toEqual([
187-
4991154550.953981, 5002306111.529594, 5005747970.58788, 4996292893.584528,
188-
])
189-
})
190-
191178
test('sumValues', () => {
192179
expect(sumValues([], 0)).toEqual([])
193180
expect(sumValues([timeseries1], 4)).toEqual([
194-
4991154550.953981, 5002306111.529594, 5005747970.58788, 4996292893.584528,
181+
4991154550.953981,
182+
5002306111.529594,
183+
5005747970.58788,
184+
null,
195185
])
196186
// we're just including this dataset twice to show that the numbers are getting added together
197187
expect(sumValues([timeseries1, timeseries1], 4)).toEqual([
198-
9982309101.907963, 10004612223.059189, 10011495941.17576, 9992585787.169056,
188+
9982309101.907963,
189+
10004612223.059189,
190+
10011495941.17576,
191+
null,
199192
])
200193
// and for good measure, we'll include it three times
201194
expect(sumValues([timeseries1, timeseries1, timeseries1], 4)).toEqual([
202-
14973463652.861944, 15006918334.588783, 15017243911.763641, 14988878680.753584,
195+
14973463652.861944,
196+
15006918334.588783,
197+
15017243911.763641,
198+
null,
203199
])
204200
})
205201

@@ -217,7 +213,7 @@ const composedUtilizationData = {
217213
},
218214
{
219215
timestamp: 1740166303000,
220-
value: 4996292893.584528,
216+
value: null,
221217
},
222218
],
223219
timeseriesCount: 1,
@@ -235,7 +231,7 @@ const utilizationChartData = [
235231
},
236232
{
237233
timestamp: 1740166303000,
238-
value: 99.92585787169057,
234+
value: null,
239235
},
240236
]
241237

@@ -252,7 +248,7 @@ const utilizationChartData4 = [
252248
},
253249
{
254250
timestamp: 1740166303000,
255-
value: 24.981464467922642,
251+
value: null,
256252
},
257253
]
258254

app/components/oxql-metrics/util.ts

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* Copyright Oxide Computer Company
77
*/
88

9+
import * as R from 'remeda'
10+
911
import type { ChartDatum, OxqlQueryResult, Timeseries } from '~/api'
1012
import { getDurationSeconds } from '~/util/date'
1113

@@ -14,7 +16,7 @@ import { getDurationSeconds } from '~/util/date'
1416
* https://github.com/oxidecomputer/omicron/tree/main/oximeter/oximeter/schema
1517
*/
1618

17-
export type OxqlDiskMetricName =
19+
type OxqlDiskMetricName =
1820
| 'virtual_disk:bytes_read'
1921
| 'virtual_disk:bytes_written'
2022
| 'virtual_disk:failed_flushes'
@@ -26,7 +28,7 @@ export type OxqlDiskMetricName =
2628
| 'virtual_disk:reads'
2729
| 'virtual_disk:writes'
2830

29-
export type OxqlVmMetricName = 'virtual_machine:vcpu_usage'
31+
type OxqlVmMetricName = 'virtual_machine:vcpu_usage'
3032

3133
export type OxqlNetworkMetricName =
3234
| 'instance_network_interface:bytes_received'
@@ -69,7 +71,8 @@ export type OxqlQuery = {
6971

7072
// the interval (in seconds) at which oximeter polls for new data; a constant in Propolis
7173
// in time, we'll need to make this dynamic
72-
export const VCPU_KSTAT_INTERVAL = 5
74+
// https://github.com/oxidecomputer/propolis/blob/42b87359/bin/propolis-server/src/lib/stats/mod.rs#L54-L57
75+
export const VCPU_KSTAT_INTERVAL_SEC = 5
7376

7477
// Returns 0 if there are no data points
7578
export const getLargestValue = (data: ChartDatum[]) =>
@@ -81,17 +84,14 @@ export const getMaxExponent = (largestValue: number, base: number) =>
8184
/** convert to UTC and return the timezone-free format required by OxQL, without milliseconds */
8285
export const oxqlTimestamp = (date: Date) => date.toISOString().replace(/\.\d+Z$/, '.000')
8386

84-
export const getTimestamps = (timeseries: Timeseries): number[] =>
85-
timeseries?.points.timestamps.map((t) => new Date(t).getTime()) || []
86-
8787
/** determine the mean window, in seconds, for the given time range;
8888
* datapoints = the number of datapoints we want to see in the chart
8989
* (default is 60, to show 1 point per minute on a 1-hour chart)
9090
* */
9191
export const getMeanWithinSeconds = (start: Date, end: Date, datapoints = 60) => {
9292
const duration = getDurationSeconds({ start, end })
9393
// 5 second minimum, to handle oximeter logging interval for CPU data
94-
return Math.max(VCPU_KSTAT_INTERVAL, Math.round(duration / datapoints))
94+
return Math.max(VCPU_KSTAT_INTERVAL_SEC, Math.round(duration / datapoints))
9595
}
9696

9797
export const getTimePropsForOxqlQuery = (
@@ -108,25 +108,19 @@ export const getTimePropsForOxqlQuery = (
108108
return { meanWithinSeconds, adjustedStart }
109109
}
110110

111-
/** get the values array from the timeseries */
112-
// the shape of this data is a bit confusing; check the tests for an example
113-
export const getValuesFromTimeseries = (timeseries: Timeseries) =>
114-
timeseries.points.values[0]?.values?.values || []
115-
116-
export const sumValues = (timeseries: Timeseries[], arrLen: number): (number | null)[] => {
117-
// default to null, so missing data doesn't show on chart as "0"
118-
const summedValues: (number | null)[] = Array.from({ length: arrLen }, () => null)
119-
timeseries.forEach((ts) => {
120-
const values = getValuesFromTimeseries(ts)
121-
// add each value to the corresponding index in the summedValues array
122-
values.forEach((v, idx) => {
123-
if (v === null) return
124-
if (summedValues[idx] === null) summedValues[idx] = 0
125-
summedValues[idx] += Number(v)
126-
})
127-
})
128-
return summedValues
129-
}
111+
export const sumValues = (timeseries: Timeseries[], arrLen: number): (number | null)[] =>
112+
Array.from({ length: arrLen }).map((_, i) =>
113+
R.pipe(
114+
timeseries,
115+
// get point at that index for each timeseries
116+
R.map((ts) => ts.points.values.at(0)?.values.values?.[i]),
117+
// filter out nulls (undefined shouldn't happen)
118+
R.filter((p) => typeof p == 'number'),
119+
// null if no timeseries has a data point at that idx, so empty parts of
120+
// the chart stay empty
121+
(points) => (points.length > 0 ? R.sum(points) : null)
122+
)
123+
)
130124

131125
type ChartUnitType = 'Bytes' | '%' | 'Count'
132126
export const getUnit = (title: string): ChartUnitType => {
@@ -146,14 +140,12 @@ export const composeOxqlData = (data: OxqlQueryResult | undefined) => {
146140
timeseriesCount = timeseriesData.length
147141
if (!timeseriesCount) return { chartData: [], timeseriesCount }
148142
// Extract timestamps (all series should have the same timestamps)
149-
const timestamps = getTimestamps(timeseriesData[0])
143+
const timestamps =
144+
timeseriesData[0]?.points.timestamps.map((t) => new Date(t).getTime()) || []
150145
// Sum up the values across all time series
151146
const summedValues = sumValues(timeseriesData, timestamps.length)
152147
const chartData = timestamps
153-
.map((timestamp, idx) => ({
154-
timestamp,
155-
value: summedValues[idx],
156-
}))
148+
.map((timestamp, idx) => ({ timestamp, value: summedValues[idx] }))
157149
// Drop the first datapoint, which — for delta metric types — is the cumulative sum of all previous
158150
// datapoints (like CPU utilization). We've accounted for this by adjusting the start time earlier;
159151
// We could use a more elegant approach to this down the road
@@ -222,7 +214,7 @@ export const getUtilizationChartProps = (
222214
nCPUs: number
223215
): OxqlMetricChartProps => {
224216
// The divisor is the oximeter logging interval for CPU data (5 seconds) * 1,000,000,000 (nanoseconds) * nCPUs
225-
const divisor = VCPU_KSTAT_INTERVAL * 1000 * 1000 * 1000 * nCPUs
217+
const divisor = VCPU_KSTAT_INTERVAL_SEC * 1000 * 1000 * 1000 * nCPUs
226218
const data =
227219
// dividing by 0 would blow it up, so on the off chance that timeSeriesCount is 0, data should be an empty array
228220
divisor > 0

0 commit comments

Comments
 (0)