Skip to content

Commit 9e0f121

Browse files
committed
Collect the geometric mean and also a regular histogram
Plotting a histogram with uneven bucket sizes, such as we get from the HDR histogram, is fiddly and doesn't give us the granularity we want
1 parent dc3fe6f commit 9e0f121

File tree

3 files changed

+93
-8
lines changed

3 files changed

+93
-8
lines changed

app/queries/src/executors/base/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function parseHdrHistogramText(text: string): HDRHistogramParsedStats[] {
3333
export function makeBenchmarkMetrics(
3434
params: BenchmarkMetricParams
3535
): BenchmarkMetrics {
36-
const { name, histogram, time, requests, response } = params
36+
const { name, histogram, basicHistogram, time, requests, response, geoMean } = params
3737
return {
3838
name,
3939
time,
@@ -43,11 +43,13 @@ export function makeBenchmarkMetrics(
4343
json: {
4444
...histogram.toJSON(),
4545
mean: histogram.mean,
46+
geoMean,
4647
min: histogram.min,
4748
stdDeviation: histogram.stdDeviation,
4849
},
4950
parsedStats: histogram.parsedStats,
5051
},
52+
basicHistogram,
5153
}
5254
}
5355

app/queries/src/executors/base/types.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ export interface HDRHistogramParsedStats {
133133
ofOnePercentile: string
134134
}
135135

136-
export interface HistogramSummaryWithMeanMinAndStdDev extends HistogramSummary {
136+
// add some extra statistics:
137+
export interface HistogramSummaryWithEtc extends HistogramSummary {
137138
mean: number
139+
geoMean?: number
138140
min: number
139141
stdDeviation: number
140142
}
@@ -154,9 +156,11 @@ export interface BenchmarkMetrics {
154156
bytesPerSecond: number
155157
}
156158
histogram: {
157-
json: HistogramSummaryWithMeanMinAndStdDev
159+
json: HistogramSummaryWithEtc
158160
parsedStats: HDRHistogramParsedStats[]
159161
}
162+
// A basic histogram with equal size buckets (the hdr histogram above predates this):
163+
basicHistogram?: HistBucket[]
160164
// These are available when 'extended_hasura_checks: true' in the config yaml:
161165
extended_hasura_checks?: {
162166
bytes_allocated_per_request: number
@@ -172,6 +176,7 @@ export interface BenchmarkMetrics {
172176
export interface BenchmarkMetricParams {
173177
name: string
174178
histogram: precise_hdr.PreciseHdrHistogram
179+
basicHistogram?: HistBucket[]
175180
time: {
176181
start: Date | string
177182
end: Date | string
@@ -183,5 +188,15 @@ export interface BenchmarkMetricParams {
183188
response: {
184189
totalBytes: number
185190
bytesPerSecond: number
186-
}
191+
},
192+
// geometric mean of service times
193+
geoMean?: number
194+
}
195+
196+
// See histogram()
197+
//
198+
// there are 'count' values in the bucket greater than 'gte'
199+
export interface HistBucket {
200+
gte: number,
201+
count: number
187202
}

app/queries/src/executors/k6/index.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MaxRequestsInDurationBenchmark,
1313
MultiStageBenchmark,
1414
RequestsPerSecondBenchmark,
15+
HistBucket,
1516
} from '../base/types'
1617

1718
import {
@@ -213,14 +214,20 @@ export class K6Executor extends BenchmarkExecutor {
213214
crlfDelay: Infinity,
214215
})
215216

216-
// Create a histogram, record each HTTP Request duration in it
217-
const histogram = hdr.build()
217+
// We'll build an hdr histogram of HTTP Request durations
218+
const hdrHistogram = hdr.build()
219+
// ...and record raw durations for processing:
220+
var reqDurations: number[] = []
221+
218222
for await (const line of rl) {
219223
const stat: K6Metric | K6Point = JSON.parse(line)
224+
// filter for just service time of successful queries:
220225
if (stat.type != 'Point') continue
221226
if (stat.metric != 'http_req_duration') continue
222227
if (Number(stat.data.tags.status) < 200) continue
223-
histogram.recordValue(stat.data.value)
228+
hdrHistogram.recordValue(stat.data.value)
229+
230+
reqDurations.push(stat.data.value)
224231
}
225232

226233
// Remove the temp config file with the K6 run parameters, and logging stats
@@ -231,7 +238,8 @@ export class K6Executor extends BenchmarkExecutor {
231238
const jsonStats: K6Summary = fs.readJSONSync(outPath)
232239
const metrics = makeBenchmarkMetrics({
233240
name: metadata.queryName,
234-
histogram,
241+
histogram: hdrHistogram,
242+
basicHistogram: histogram(200, reqDurations),
235243
time: {
236244
start: benchmarkStart.toISOString(),
237245
end: benchmarkEnd.toISOString(),
@@ -244,8 +252,68 @@ export class K6Executor extends BenchmarkExecutor {
244252
totalBytes: jsonStats.metrics.data_received.count,
245253
bytesPerSecond: jsonStats.metrics.data_received.rate,
246254
},
255+
geoMean: geoMean(reqDurations)
247256
})
248257

249258
return metrics
250259
}
251260
}
261+
262+
// geometric mean, with exponent distributed over product so we don't overflow
263+
function geoMean(xs: number[]): number {
264+
return xs.map(x => Math.pow(x, 1/xs.length)).reduce((acc, x) => acc * x)
265+
}
266+
267+
// NOTE: To save space and aid readability we’ll filter out any buckets with a
268+
// count of 0 that follow a bucket with a count of 0. This can still be graphed
269+
// fine without extra accommodations using a stepped line plot, as we plan
270+
function histogram(numBuckets: number, xs: number[]): HistBucket[] {
271+
if (numBuckets < 1 || xs.length < 2) { throw "We need at least one bucket and xs.length > 1" }
272+
273+
var xsSorted = new Float64Array(xs) // so sort works properly
274+
xsSorted.sort()
275+
276+
const bucketWidth = (xsSorted[xsSorted.length - 1] - xsSorted[0]) / numBuckets
277+
278+
var buckets = []
279+
for (let gte = xsSorted[0] ; true ; gte+=bucketWidth) {
280+
// Last bucket; add remaining and stop
281+
if (buckets.length === (numBuckets-1)) {
282+
buckets.push({gte, count: xsSorted.length})
283+
break
284+
}
285+
var count = 0
286+
var ixNext
287+
// this should always consume as least one value:
288+
xsSorted.some((x, ix) => {
289+
if (x < (gte+bucketWidth)) {
290+
count++
291+
return false // i.e. keep looping
292+
} else {
293+
ixNext = ix
294+
return true
295+
}
296+
})
297+
if (ixNext === undefined) {throw "Bugs in histogram!"}
298+
xsSorted = xsSorted.slice(ixNext)
299+
buckets.push({gte, count})
300+
}
301+
// having at most one 0 bucket in a row, i.e. `{gte: n, count: 0}` means
302+
// "This and all higher buckets are empty"
303+
var bucketsSparse = []
304+
var inAZeroSpan = false
305+
buckets.forEach( b => {
306+
if (inAZeroSpan && b.count === 0) {
307+
// drop this bucket
308+
} else if (!inAZeroSpan && b.count === 0) {
309+
// include this zero buckets but not subsequent zero buckets
310+
bucketsSparse.push(b)
311+
inAZeroSpan = true
312+
} else {
313+
inAZeroSpan = false
314+
bucketsSparse.push(b)
315+
}
316+
})
317+
318+
return bucketsSparse
319+
}

0 commit comments

Comments
 (0)