Benchmark
The Benchmark package allows you to easily create sophisticated Swift performance benchmarks for a wide variety of metrics.
This README provides a quick overview of the Benchmark package. For more detailed information, please see the documentation.
Overview
Performance is a key feature for many apps and frameworks. Benchmark helps make it easy to measure and track many different metrics that affects performance, such as CPU usage, ARC traffic, memory/malloc usage and use of operating system resources such as threads and system calls, as well as completely custom metric counters.
Benchmark works on both macOS and Linux and supports several key workflows for performance measurements:
- Automated Pull Request performance regression checks by comparing the performance metrics of a pull request with the main branch and having the PR workflow check fail if there is a regression according to absolute or relative threshold tolerances specified per benchmark
- Manual comparison of multiple performance baselines for iterative or A/B performance work by an individual developer
- Export of benchmark results in several formats for analysis or visualization
Benchmark is suitable for both smaller ad-hoc benchmarks focusing on execution time and more extensive benchmarks that care about several additional metrics such as memory allocations, syscalls, thread usage, context switches, ARC traffic, and more. Using Histogram it’s especially suitable for capturing latency statistics for large number of samples.
Documentation
Documentation on how to use Benchmark in your Swift package can be viewed online or inside Xcode using Build Documentation.
Additionally the command plugin provides help information if you run swift package benchmark help from the command line.
Adding dependencies and getting started
There are just a few steps required to get started benchmarking:
- Add a dependency to the Benchmark project
- Add benchmark executable targets with
swift package benchmark init - Add the snippet or code you want to benchmark
- Run
swift package benchmark
Step 1: Add a package dependency to Package.swift
To add the dependency on Benchmark, add the dependency to your package:.package(url: "https://github.com/ordo-one/benchmark", .upToNextMajor(from: "1.4.0")),
Step 2: Add benchmark exectuable targets using benchmark init
The absolutely easiest way to add new benchmark executable targets to your project is by using:
swift package --allow-writing-to-package-directory benchmark init MyNewBenchmarkTarget
This will perform the following steps for you:
- Create a Benchmarks/MyNewBenchmarkTarget directory
- Create a Benchmarks/MyNewBenchmarkTarget/MyNewBenchmarkTarget.swift benchmark target with the required boilerplate
- Add a new executable target for the benchmark to the end of your Package.swift file
After you’ve created the new target, you can directly run it with e.g.:
swift package benchmark --target MyNewBenchmarkTarget
Step 2 (optional approach): Add benchmark exectuable targets manually
Alternatively if you don't want the plugin to modify your project directory, you can do the same steps manually: Create an executable target in Package.swift for each benchmark suite you want to measure. The source for all benchmarks must reside in a directory namedBenchmarks in the root of your swift package.
The benchmark plugin uses this directory combined with the executable target information to automatically discover and run your benchmarks.
For each executable target, include dependencies on both Benchmark (supporting framework) and BenchmarkPlugin (boilerplate generator) from the benchmark package.
The following example shows an benchmark suite named My-Benchmark with the required dependency on Benchmark and the source files for the benchmark that reside in the directory Benchmarks/My-Benchmark:
.executableTarget(
name: "My-Benchmark",
dependencies: [
.product(name: "Benchmark", package: "benchmark"),
],
path: "Benchmarks/My-Benchmark",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "benchmark"),
]
),
Step 3: Writing benchmarks
There are documentation available as well as a a sample project using various aspects of this package in practice.Sample benchmark code
import Benchmark
let benchmarks : @Sendable () -> Void = {
Benchmark("Minimal benchmark") { benchmark in
// measure something here
}
func defaultCounter() -> Int {
10
}
func dummyCounter(_ count: Int) {
for index in 0 ..< count {
blackHole(index)
}
}
Benchmark("All metrics, full concurrency, async",
configuration: .init(metrics: BenchmarkMetric.all,
maxDuration: .seconds(10))) { benchmark in
let _ = await withTaskGroup(of: Void.self, returning: Void.self, body: { taskGroup in
for _ in 0..<80 {
taskGroup.addTask {
dummyCounter(defaultCounter()*1000)
}
}
for await _ in taskGroup {
}
})
}
}
Step 4: Running benchmarks
To execute all defined benchmarks, simply run:``swift package benchmarkPlease see the documentation for more detail on all options.
Sample output benchmark run
<img width="1005" alt="image" src="https://user-images.githubusercontent.com/8501048/225311258-1247f8e9-c1fd-4598-a4b8-2b41a9b9a8e7.png">Sample output benchmark grouped by metric
<img width="1089" alt="image" src="https://user-images.githubusercontent.com/8501048/225281786-411530de-25c2-47b5-b99f-0d7bac3209a7.png">Sample output delta comparison
<img width="1173" alt="image" src="https://user-images.githubusercontent.com/8501048/225282373-e7ba9fa1-1a2a-4028-b053-9f3aa82361b0.png">Sample output threshold deviation check
<img width="956" alt="image" src="https://user-images.githubusercontent.com/8501048/225282982-95c9c641-9455-4df2-81bc-6aee43721223.png">Sample usage of YouPlot
Install YouPlotbash swift package benchmark run --filter InternalUTCClock-now --metric wallClock --format histogramPercentiles --path stdout --no-progress | uplot lineplot -Hsh swift package benchmark run --allocation-stacks --target MyBenchmarks --filter "Encode.*"<img width="523" alt="image" src="https://user-images.githubusercontent.com/8501048/225284254-c1349494-2323-4460-b18a-7bc2896b5dc4.png">--allocation-stacksJMH Visualization
Using jmh.morethan.io
<img width="1262" alt="image" src="https://user-images.githubusercontent.com/8501048/225313246-4369da1f-0890-4856-8fd8-b28d56d842aa.png">
<img width="1482" alt="image" src="https://user-images.githubusercontent.com/8501048/225313559-33014755-797f-4ddf-b536-24c1a618f271.png">
Finding allocation sites
An allocation stack is the chain of function calls captured when heap memory is allocated, showing which code path caused the allocation.
To see where a benchmark allocates, run it with
. The stack trace of every allocation in the measured region is recorded and the unique stacks are printed sorted by allocation count:
sh swift package --allow-writing-to-package-directory benchmark run \ --export-allocation-stacks --target MyBenchmarks --filter "Encode.*"### Sample output of benchmark runed with allocation stacks recordingstartMeasurement()!Terminal output showing an allocation stack report
Warmup iterations and code outside
/stopMeasurement()are excluded. By default, up to 64 frames are captured per stack and the 20 most frequent stacks are printed. Use--allocation-stack-depth <depth>to change the capture depth and--allocation-stack-limit <limit>to change the number printed (0prints all).mallocCountTotalRecording slows allocations down, so this diagnostic mode measures only
andmallocBytesCountand is available with theruncommand. It requires theMallocInterposertrait (enabled by default) and a toolchain providing the SwiftRuntimemodule. On macOS, it requires macOS 26 or later.--export-allocation-stacksExporting folded stacks
Use
to capture, print, and export allocation stacks. It implies--allocation-stacksand defaults to thefoldedformat, which can be opened in speedscope or rendered with FlameGraph:
sh flamelens MyBenchmarks.Encode.allocations.foldedThis writes a<target>.<benchmark>.allocations.foldedfile in the current directory for each benchmark that records allocations. Folded files contain every captured stack, regardless of--allocation-stack-limit, weighted by exact allocation counts across all measured iterations.--allocation-stacks-export-path <directory>Set
to choose another destination. Grant the plugin write permission with--allow-writing-to-package-directoryfor output inside the package, or--allow-writing-to-directory <directory>for an external destination. TVisualization of the folded file with Flamelens:
sh swift package --allow-writing-to-package-directory benchmark run \ --export-allocation-stacks --allocation-stacks-export-format json \ --allocation-stacks-export-path allocation-stacks \ --target MyBenchmarks --filter "Encode.*"!Allocation flame graph displayed in Flamelens--allocation-stacks-export-format jsonExporting JSON
Select
to export a full report as<target>.<benchmark>.allocations.json, including allocation counts, byte totals, and frames:
sh swift package benchmark run \ --export-allocation-stacks --allocation-stacks-export-format json \ --allocation-stacks-export-path stdout --target MyBenchmarks --filter "^Encode$"JSON also includes every captured stack and writes reports for benchmarks with no allocations. Both--allocation-stacks-export-formatand--allocation-stacks-export-pathrequire--export-allocation-stacks;--allocation-stacksalone prints reports without creating files.--allocation-stacks-export-path stdoutTo pipe the selected format to another tool, use
. This prints only the exported data. Select one benchmark when you need a single JSON report:
See Finding Allocation Sites for report examples and more details.
Swift 6 support
The package supports Swift 6.0 benchmark targets as well as Swift 5.10 targets (for Swift 5.9 support, need to use version 1.28.0 exactly).
For Swift 6 targets, use the following signature for the benchmarks closure:
swift
let benchmarks : @Sendable () -> Void = {
`
Output
The default text output from Benchmark is oriented around the five-number summary percentiles, plus the last decile (
p90) and the last percentile (p99) - it's thus a variation of a seven-figure summary with the focus on the 'bad' end of results (as those are what we typically care about addressing).
We've found that focusing on percentiles rather than average or standard deviations, is more useful for a wider range of benchmark measurements and gives a deeper understanding of the results.
Percentiles allows for a consistent way of expressing benchmark results of both throughput and latency measurements (which typically do not have a standardized distribution, being almost always multi-modal in nature).
This multi-modal nature of the latency measurements leads to the common statistical measures of mean and standard deviation being potentially misleading.
Please see the documentation for further details on percentiles.
API and file format stability
The API will be deemed stable as of 1.0.0 and follows semantical versioning for future releases.
The export file formats that are externally defined (e.g. JMH or HDR Histogram formats) will follow the upstream definitions if they change, but have been quite stable for several years.
The Histogram codable representation is not stable and may change if the Histogram implementation changes.
The benchmark internal baseline representation (stored in
.benchmarkBaselines`) is not stable and is not viewed as public API and may break over time.
For those wanting to save benchmark data over time, it's recommended to export data in e.g. HDR Histogram representations (percentiles, average, stddev etc) or simply post processing the histogramSamples format (which is raw data) to your desired representation.
PR:s for additional standardized formats are welcome, as the export formats are the intended stable interface for saving such data.