portico/Benchmarks/Sources/PorticoBench/Harness.swift

113 lines
No EOL
3.8 KiB
Swift

import CBenchSupport
import Foundation
/// One measured result: wall time and net RSS growth.
///
/// Allocation counts are obtained separately via `heaptrack`, which is run as
/// an `LD_PRELOAD` wrapper around the executable and produces far more accurate
/// per-call-site allocation data than in-process counting could.
struct Measurement {
var name: String
var iterations: Int
var nanos: UInt64
var rssDeltaKB: Int64
var nanosPerIteration: Double { Double(nanos) / Double(iterations) }
}
/// Runs scenarios with timing. Allocation profiling is handled externally by
/// `heaptrack`; this harness measures wall time and RSS growth only.
@MainActor enum Harness {
private static var results: [Measurement] = []
/// Measures `body` over `iterations` calls after `warmup` unmeasured calls.
///
/// - Parameters:
/// - name: Scenario label printed in the report.
/// - iterations: Number of measured calls.
/// - warmup: Number of unmeasured calls first, so lazy globals, GType
/// registration, and CSS parsing land outside the window.
/// - body: The work under test. Receives the zero-based iteration index.
@discardableResult
static func measure(
_ name: String,
iterations: Int,
warmup: Int = 1,
_ body: (Int) -> Void
) -> Measurement {
for i in 0..<warmup { body(i) }
let rssBefore = bench_rss_kb()
let start = bench_now_ns()
for i in 0..<iterations { body(i) }
let elapsed = bench_now_ns() - start
let rssAfter = bench_rss_kb()
let m = Measurement(
name: name,
iterations: iterations,
nanos: elapsed,
rssDeltaKB: Int64(rssAfter) - Int64(rssBefore)
)
results.append(m)
return m
}
/// Measures a scenario whose setup must not be counted.
///
/// `setup` runs outside the measurement window and produces a value passed
/// to `body`, which is the only thing measured. Used for teardown and diff
/// scenarios where building the fixture dwarfs the operation under test.
@discardableResult
static func measureWithSetup<T>(
_ name: String,
iterations: Int,
setup: (Int) -> T,
_ body: (T) -> Void
) -> Measurement {
var fixtures: [T] = []
fixtures.reserveCapacity(iterations)
for i in 0..<iterations { fixtures.append(setup(i)) }
let rssBefore = bench_rss_kb()
let start = bench_now_ns()
for f in fixtures { body(f) }
let elapsed = bench_now_ns() - start
let rssAfter = bench_rss_kb()
let m = Measurement(
name: name,
iterations: iterations,
nanos: elapsed,
rssDeltaKB: Int64(rssAfter) - Int64(rssBefore)
)
results.append(m)
return m
}
/// Prints a fixed-width report of every recorded measurement.
static func report() {
let nameWidth = max(46, results.map(\.name.count).max() ?? 46)
func pad(_ s: String, _ w: Int) -> String {
s.count >= w ? s : s + String(repeating: " ", count: w - s.count)
}
func lpad(_ s: String, _ w: Int) -> String {
s.count >= w ? s : String(repeating: " ", count: w - s.count) + s
}
print("")
print(
pad("scenario", nameWidth) + lpad("iters", 8) + lpad("ns/iter", 14)
+ lpad("rss KB", 10)
)
print(String(repeating: "-", count: nameWidth + 32))
for r in results {
print(
pad(r.name, nameWidth)
+ lpad("\(r.iterations)", 8)
+ lpad(String(format: "%.0f", r.nanosPerIteration), 14)
+ lpad("\(r.rssDeltaKB)", 10)
)
}
print("")
}
}