6
6
* Copyright Oxide Computer Company
7
7
*/
8
8
9
+ import * as R from 'remeda'
10
+
9
11
import type { ChartDatum , OxqlQueryResult , Timeseries } from '~/api'
10
12
import { getDurationSeconds } from '~/util/date'
11
13
@@ -14,7 +16,7 @@ import { getDurationSeconds } from '~/util/date'
14
16
* https://github.com/oxidecomputer/omicron/tree/main/oximeter/oximeter/schema
15
17
*/
16
18
17
- export type OxqlDiskMetricName =
19
+ type OxqlDiskMetricName =
18
20
| 'virtual_disk:bytes_read'
19
21
| 'virtual_disk:bytes_written'
20
22
| 'virtual_disk:failed_flushes'
@@ -26,7 +28,7 @@ export type OxqlDiskMetricName =
26
28
| 'virtual_disk:reads'
27
29
| 'virtual_disk:writes'
28
30
29
- export type OxqlVmMetricName = 'virtual_machine:vcpu_usage'
31
+ type OxqlVmMetricName = 'virtual_machine:vcpu_usage'
30
32
31
33
export type OxqlNetworkMetricName =
32
34
| 'instance_network_interface:bytes_received'
@@ -69,7 +71,8 @@ export type OxqlQuery = {
69
71
70
72
// the interval (in seconds) at which oximeter polls for new data; a constant in Propolis
71
73
// 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
73
76
74
77
// Returns 0 if there are no data points
75
78
export const getLargestValue = ( data : ChartDatum [ ] ) =>
@@ -81,17 +84,14 @@ export const getMaxExponent = (largestValue: number, base: number) =>
81
84
/** convert to UTC and return the timezone-free format required by OxQL, without milliseconds */
82
85
export const oxqlTimestamp = ( date : Date ) => date . toISOString ( ) . replace ( / \. \d + Z $ / , '.000' )
83
86
84
- export const getTimestamps = ( timeseries : Timeseries ) : number [ ] =>
85
- timeseries ?. points . timestamps . map ( ( t ) => new Date ( t ) . getTime ( ) ) || [ ]
86
-
87
87
/** determine the mean window, in seconds, for the given time range;
88
88
* datapoints = the number of datapoints we want to see in the chart
89
89
* (default is 60, to show 1 point per minute on a 1-hour chart)
90
90
* */
91
91
export const getMeanWithinSeconds = ( start : Date , end : Date , datapoints = 60 ) => {
92
92
const duration = getDurationSeconds ( { start, end } )
93
93
// 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 ) )
95
95
}
96
96
97
97
export const getTimePropsForOxqlQuery = (
@@ -108,25 +108,19 @@ export const getTimePropsForOxqlQuery = (
108
108
return { meanWithinSeconds, adjustedStart }
109
109
}
110
110
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
+ )
130
124
131
125
type ChartUnitType = 'Bytes' | '%' | 'Count'
132
126
export const getUnit = ( title : string ) : ChartUnitType => {
@@ -146,14 +140,12 @@ export const composeOxqlData = (data: OxqlQueryResult | undefined) => {
146
140
timeseriesCount = timeseriesData . length
147
141
if ( ! timeseriesCount ) return { chartData : [ ] , timeseriesCount }
148
142
// 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 ( ) ) || [ ]
150
145
// Sum up the values across all time series
151
146
const summedValues = sumValues ( timeseriesData , timestamps . length )
152
147
const chartData = timestamps
153
- . map ( ( timestamp , idx ) => ( {
154
- timestamp,
155
- value : summedValues [ idx ] ,
156
- } ) )
148
+ . map ( ( timestamp , idx ) => ( { timestamp, value : summedValues [ idx ] } ) )
157
149
// Drop the first datapoint, which — for delta metric types — is the cumulative sum of all previous
158
150
// datapoints (like CPU utilization). We've accounted for this by adjusting the start time earlier;
159
151
// We could use a more elegant approach to this down the road
@@ -222,7 +214,7 @@ export const getUtilizationChartProps = (
222
214
nCPUs : number
223
215
) : OxqlMetricChartProps => {
224
216
// 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
226
218
const data =
227
219
// dividing by 0 would blow it up, so on the off chance that timeSeriesCount is 0, data should be an empty array
228
220
divisor > 0
0 commit comments