Skip to content

Commit

Permalink
test: Add a better benchmark util for typescript (#3959)
Browse files Browse the repository at this point in the history
This function does a few things:
- Has a configurable warm-up period, to settle V8 hot-path optimizations
  and whatever else
- Has a configurable number of iterations which is used to collect
  aggregate information
- Calculates the min, max, avg, median across the iterations. Does not
  include the warm-up iterations
- Renders the aggregate stats
  • Loading branch information
mat-if authored Jun 9, 2023
1 parent 511cd28 commit b2efb1a
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 1 deletion.
96 changes: 96 additions & 0 deletions ironfish/src/utils/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { FileUtils } from './file'
import { MathUtils } from './math'
import { TimeUtils } from './time'

export type SegmentResults = {
Expand All @@ -11,6 +12,21 @@ export type SegmentResults = {
mem: number
}

type Aggregate = {
min: number
max: number
avg: number
median: number
}

export type SegmentAggregateResults = {
iterations: number
time: Aggregate
heap: Aggregate
rss: Aggregate
mem: Aggregate
}

type Segment = {
time: HRTime
heap: number
Expand Down Expand Up @@ -94,17 +110,97 @@ function renderSegment(segment: SegmentResults, title = 'Benchmark', delimiter =
return rendered
}

function renderSegmentAggregate(
segmentAggregate: SegmentAggregateResults,
title = 'Benchmark',
delimiter = '\n',
): string {
const result = []

const renderAggregate = (
name: string,
aggregate: Aggregate,
renderFn: (arg: number) => string,
): string => {
return `${name}: min: ${renderFn(aggregate.min)}, avg: ${renderFn(
aggregate.avg,
)}, median: ${renderFn(aggregate.median)}, max ${renderFn(aggregate.max)}`
}

result.push(`Iterations: ${segmentAggregate.iterations}`)
result.push(renderAggregate('Time', segmentAggregate.time, TimeUtils.renderSpan))
result.push(renderAggregate('Heap', segmentAggregate.heap, FileUtils.formatMemorySize))
result.push(renderAggregate('Rss', segmentAggregate.rss, FileUtils.formatMemorySize))
result.push(renderAggregate('Mem', segmentAggregate.mem, FileUtils.formatMemorySize))

let rendered = result.join(delimiter)

if (title) {
rendered = `${title}` + delimiter + rendered
}

return rendered
}

async function withSegment(fn: () => Promise<void> | void): Promise<SegmentResults> {
const segment = startSegment()
await fn()
return endSegment(segment)
}

async function withSegmentIterations(
warmupIterations: number,
testIterations: number,
fn: () => Promise<void> | void,
): Promise<SegmentAggregateResults> {
for (let i = 0; i < warmupIterations; i++) {
await fn()
}

const results: Array<SegmentResults> = []
for (let i = 0; i < testIterations; i++) {
results.push(await withSegment(fn))
}

return aggregateResults(results)
}

function aggregateResults(results: SegmentResults[]): SegmentAggregateResults {
const assignResults = (key: Aggregate, sortedArray: number[]) => {
key.min = sortedArray[0]
key.max = sortedArray[time.length - 1]
key.avg = MathUtils.arrayAverage(sortedArray)
key.median = MathUtils.arrayMedian(sortedArray, true)
}

const aggregateResults: SegmentAggregateResults = {
iterations: results.length,
time: { min: 0, max: 0, avg: 0, median: 0 },
heap: { min: 0, max: 0, avg: 0, median: 0 },
rss: { min: 0, max: 0, avg: 0, median: 0 },
mem: { min: 0, max: 0, avg: 0, median: 0 },
}

const time = results.map((r) => r.time).sort()
const heap = results.map((r) => r.heap).sort()
const rss = results.map((r) => r.rss).sort()
const mem = results.map((r) => r.mem).sort()

assignResults(aggregateResults.time, time)
assignResults(aggregateResults.heap, heap)
assignResults(aggregateResults.rss, rss)
assignResults(aggregateResults.mem, mem)

return aggregateResults
}

export const BenchUtils = {
start: startTime,
end: endTime,
startSegment,
endSegment,
renderSegment,
renderSegmentAggregate,
withSegment,
withSegmentIterations,
}
27 changes: 26 additions & 1 deletion ironfish/src/utils/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ function arrayAverage(values: number[]): number {
return total / values.length
}

function arrayMedian(values: number[], isSorted = false): number {
if (values.length === 0) {
return 0
}

// TODO(mat): We can use values.toSorted() when we set NodeJS min version to 20
const sorted = isSorted ? values : [...values].sort()

const half = Math.floor(sorted.length / 2)
if (sorted.length % 2) {
return sorted[half]
}

return sorted[half - 1] + values[half] / 2
}

function arraySum(values: number[]): number {
if (values.length === 0) {
return 0
Expand Down Expand Up @@ -56,4 +72,13 @@ function min<T extends number | bigint>(a: T, b: T): T {
return a > b ? b : a
}

export const MathUtils = { arrayAverage, arraySum, round, roundBy, min, max, floor }
export const MathUtils = {
arrayAverage,
arrayMedian,
arraySum,
round,
roundBy,
min,
max,
floor,
}

0 comments on commit b2efb1a

Please sign in to comment.