113 lines
No EOL
3.8 KiB
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("")
|
|
}
|
|
} |