Pre-requisites: git and bazel (or bazelisk; .bazelversion pins Bazel 9.x with Bzlmod).
# Clone bazel-bench.
$ git clone https://github.com/bazelbuild/bazel-bench.git
$ cd bazel-bench
To do a test run, run the following command:
$ bazel run :benchmark \
-- \
--bazel_commits=b8468a6b68a405e1a5767894426d3ea9a1a2f22f,ad503849e78b98d762f03168de5a336904280150 \
--project_source=https://github.com/bazelbuild/rules_cc.git \
--verbose \
-- build //:allThe Bazel commits above might be too old and no longer buildable by your local Bazel. Replace them with more recent commits from bazelbuild/bazel, or pass pre-built binaries with --bazel_binaries=/path/to/bazel_a,/path/to/bazel_b.
The command prints a formatted summary table on the terminal (comparing every unit against the first unit as the baseline, including mean, ±stddev, Δ mean, and speedup ratio with statistical significance) and automatically exports CSV (<uid>.csv), plain-text summary (<uid>.txt), Markdown (<uid>.md), and structured JSON (<uid>.json) reports to --data_directory. If --data_directory is not specified, bazel-bench creates a temporary directory /tmp/benchmark.<random> and updates a /tmp/benchmark.latest symlink pointing to the most recent run's output directory.
Bazel-bench has the following syntax:
$ bazel run :benchmark -- <bazel-bench-flags> -- <args to pass to bazel binary>For example, to benchmark the performance of 2 bazel commits A and B on the same
command bazel build --nobuild //:all of rules_cc project, you'd do:
$ bazel run :benchmark \
-- \
--bazel_commits=A,B \
--project_source=https://github.com/bazelbuild/rules_cc.git \
-- build --nobuild //:allNote the double-dash -- before the command arguments. You can pass any
arguments that you would normally run on Bazel to the script. The performance of
commands other than build can also be benchmarked e.g. query, ...
- Pre-built Bazel binaries (
--bazel_binaries): When--bazel_binariesis passed (without--bazel_commitsor--bisect),bazel-benchvalidates that the binaries exist and are executable and skips cloning the Bazel repository. - In-place local project benchmarking: When
--project_sourcepoints to a local directory and--project_commitsis omitted (orproject_commitis omitted inconfig.yaml),bazel-benchruns directly in the local project checkout without cloning it.
The flag-based approach does not support cases where the benchmarked Bazel commands differ. The most common use case for this: As a rule developer, I want to verify the effect of my flag on Bazel performance. For that, we'd need the config-file interface. The example config file would look like this:
# config.yaml
global_options:
project_commit: 595a730
runs: 5
warmup_runs: 1
collect_profile: false
collect_bep: true
project_source: /path/to/project/repo
units:
- bazel_binary: /usr/bin/bazel
command: --startup_option1 build --nomy_flag //:all
- bazel_binary: /usr/bin/bazel
command: --startup_option2 build --my_flag //:allTo launch the benchmark:
$ bazel run :benchmark -- --benchmark_config=/absolute/path/to/config.yamlThe above config file would benchmark 2 "units". A unit is defined as a set of
conditions that describes a scenario to be benchmarked. This setup allows
maximum flexibility, as the conditions are independent between units. It's even
possible to benchmark a bazel_commit against a pre-built bazel_binary.
global_options is the list of options applied to every unit. These global options are overridden by local unit options. Supported options include runs, warmup_runs, max_outlier_reruns, interleave, clean, shutdown, collect_memory, collect_process_memory, collect_bep, collect_peak_post_gc_memory, collect_profile, patch_file, env_configure, and bazel_source.
For the list of currently supported flags/attributes and their default values, refer to utils/benchmark_config.py.
project_sourceshould be a global option, as we don't support benchmarking multiple projects in 1 benchmark. Though,project_commitcan differ between units (or be omitted to benchmark a localproject_sourcein place).- Commands have to be in canonical form (next section).
Bazel arguments are parsed manually. It is important that the supplied arguments in the command line strictly follows the canonical form:
<startup_options> <command> <canonical options> [--] <expressions>
Example of non-canonical command line arguments that could result in wrong interpretation:
GOOD: (correct order, options in canonical form)
build --nobuild --compilation_mode=opt //:all
GOOD: (using -- to separate options from target patterns)
build --nobuild -- //foo/... -//foo/excluded/...
BAD: (non-canonical options)
build --nobuild -c opt //:all
BAD: (wrong order)
build --nobuild //:all --compilation_mode=opt
- Warmup runs (
--warmup_runs, default:1): Performs discarded warmup runs before measurement runs begin. - Outlier reruns (
--max_outlier_reruns, default:0): Automatically detects wall-time outliers using Modified Z-score (Median Absolute Deviation / MAD) and reruns the worst outlier up toNtimes. - Round-robin unit interleaving (
--[no]interleave, default:false): After preparing and warming up each unit, interleaves measurement runs across units in round-robin order (Unit #0 run 1 -> Unit #1 run 1 -> ...) to mitigate thermal throttling and background system drift. - Clean and shutdown control (
--[no]clean,--[no]shutdown, default:true): Controls whetherbazel cleanandbazel shutdownare invoked between runs.
- Retained heap memory (
--[no]collect_memory, default:true): Collects retained JVM heap size (memory) after each command viabazel info used-heap-size-after-gc. Pass--nocollect_memoryto skip post-build heap inspection. - Peak process RSS (
--[no]collect_process_memory, default:false): Samples the Bazel server's anonymous RSS (RssAnonfrom/proc/<pid>/status) in a background thread during execution to reportpeakProcessRss. - BEP metrics (
--[no]collect_bep, default:false): Parses the Build Event Protocol JSON file (--build_event_json_file) after each run to extract:- Build graph & analysis:
packagesLoaded,targetsConfigured,actionsCreated,actionsExecuted,outputArtifactCount - Phase timings:
analysisPhaseTime,executionPhaseTime - Action cache & runner statistics:
actionCacheHits,actionCacheMisses,remoteCacheHits,diskCacheHits,runner-<name> - JVM heap & GC metrics:
usedHeapSizePostBuild,edenSpaceGarbage,oldGenGarbage,peakPostGcHeapSize
- Build graph & analysis:
- Peak post-GC heap memory (
--[no]collect_peak_post_gc_memory, default:false): Enables--collect_bepand automatically adds--memory_profile=/dev/null(unless--memory_profileis already specified) so Bazel recordspeakPostGcHeapSizein the BEP output.
To benchmark incremental builds, disable clean and shutdown between runs (--noclean --noshutdown) and optionally supply a git patch file via --patch_file=/path/to/change.patch (or patch_file in config.yaml).
Before each run, bazel-bench replaces every occurrence of __RANDOM_TOKEN__ in the patch file with a fresh 32-character hex token, applies the patch to the project repository, runs the build, and reverse-applies the patch afterward. Note that --patch_file requires the project to be cloned (either via a remote --project_source URL or by specifying --project_commits / project_commit), so that a local working tree is never modified in place.
bazel-bench can automatically bisect a Bazel commit range ([good, bad]) to find the first commit that caused a statistically significant regression on a target metric (e.g. wall, cpu, memory):
$ bazel run :benchmark \
-- \
--bazel_commits=<good_commit>,<bad_commit> \
--project_source=/path/to/project \
--bisect=wall \
-- build //:allAt the end of bisection, bazel-bench prints the culprit commit and evaluation history and exports <uid>_bisect.md to the data directory.
To show all the available flags:
$ bazel run :benchmark -- --helpshort
Available flags:
--[no]aggregate_json_profiles: Whether to aggregate the collected JSON
profiles. Requires --collect_profile to be set.
(default: 'false')
--bazel_bin_dir: The directory to store the bazel binaries from each commit.
--bazel_binaries: The pre-built bazel binaries to benchmark.
(a comma separated list)
--bazel_commits: The commits at which bazel is built.
(a comma separated list)
--bazel_source: Either a path to the local Bazel repo or a https url to a
GitHub repository.
(default: 'https://github.com/bazelbuild/bazel.git')
--bazelrc: The path to a .bazelrc file.
--benchmark_config: Whether to use the config-file interface to define
benchmark units.
--bisect: Run automated git bisection for regressions on the specified metric
(e.g. wall, cpu, memory). Requires --bazel_commits with exactly two commits
[good, bad].
--[no]clean: Whether to invoke clean between runs/builds.
(default: 'true')
--[no]collect_bep: Whether to collect build metrics from the Build Event
Protocol (BEP).
(default: 'false')
--[no]collect_memory: Whether to collect retained heap size via GC after
command execution.
(default: 'true')
--[no]collect_peak_post_gc_memory: Whether to collect peak post-GC heap memory
via BEP and memory profiler. Turns on --collect_bep.
(default: 'false')
--[no]collect_process_memory: Whether to sample peak anonymous process RSS
memory.
(default: 'false')
--[no]collect_profile: Whether to collect JSON profile for each run. Requires
--data_directory to be set.
(default: 'false')
--csv_file_name: The name of the output csv, without the .csv extension.
--data_directory: The directory in which the csv files should be stored.
--env_configure: The shell commands to configure the project's environment.
--[no]interleave: Whether to interleave benchmark runs across units in a
round-robin order.
(default: 'false')
--max_outlier_reruns: Maximum number of automatic reruns when a wall-time
outlier is detected.
(default: '0')
(an integer)
--patch_file: Path to a patch file containing changes to apply for incremental
builds.
--platform: The platform on which bazel-bench is run. This is just to
categorize data and has no impact on the actual script execution.
--[no]prefetch_ext_deps: Whether to do an initial run to pre-fetch external
dependencies.
(default: 'true')
--project_commits: The commits from the git project to be benchmarked.
(a comma separated list)
--project_label: The label of the project. Only relevant in the daily
performance report.
--project_source: Either a path to the local git project to be built or a
https url to a GitHub repository.
--runs: The number of benchmark runs.
(default: '5')
(an integer)
--[no]shutdown: Whether to invoke shutdown between runs/builds.
(default: 'true')
--[no]verbose: Whether to include git/Bazel stdout logs.
(default: 'false')
--warmup_runs: The number of warmup runs to perform before measurements
(discarded).
(default: '1')
(an integer)
Bazel's JSON Profile
is a useful tool to investigate the performance of Bazel. You can configure
bazel-bench to export these JSON profiles on runs using the
--collect_profile flag.
For each pair of project_commit and bazel_commit, we produce a couple JSON
profiles, based on the number of runs. To have a better overview of the
performance of each phase and events, we can aggregate these profiles and
produce the median duration of each event across them.
To run the tool:
bazel run utils:json_profiles_merger \
-- \
--bazel_source=<some commit or path> \
--project_source=<some url or path> \
--project_commit=<some_commit> \
--output_path=/tmp/outfile.csv \
-- /tmp/my_json_profiles_*.profile
You can pass the pattern that selects the input profiles into the positional
argument of the script, like in the above example
(/tmp/my_json_profiles_*.profile).
By default, bazel-bench stores cloned repositories and built Bazel binaries under ~/.bazel-bench:
~/.bazel-bench/ <= The root of bazel-bench's cache dir.
bazel-clones/ <= Where Bazel repositories are cloned (by source hash).
bazel-bin/ <= Where built Bazel binaries are stored (can be overridden via --bazel_bin_dir).
fba9a2c87ee9589d72889caf082f1029/ <= The Bazel commit hash (optionally under a <platform>/ subdir).
bazel <= The actual Bazel binary.
project-clones/ <= Where benchmarked project repositories are cloned.
7ffd56a6e4cb724ea575aba15733d113/ <= Each project is stored under an MD5 hash of its source.
Measurement outputs (.csv, .txt, .md, .json, and optional .profile.gz / .bep.json files) are written to --data_directory. When --data_directory is not specified, bazel-bench creates a temporary /tmp/benchmark.<random> directory and updates /tmp/benchmark.latest as a symlink to that directory.
To clear the caches, simply rm -rf where necessary.
As an important part of our bazel-bench daily pipeline, we upload the csv output files to BigQuery and Storage, using separate targets.
To upload the output to BigQuery & Storage you'll need the GCP credentials and the table details. Please contact leba@google.com.
BigQuery:
bazel run utils:bigquery_upload \
-- \
--upload_to_bigquery=<project_id>:<dataset_id>:<table_id>:<location> \
-- \
<file1> <file2> ...
Storage:
bazel run utils:storage_upload \
-- \
--upload_to_storage=<project_id>:<bucket_id>:<subdirectory> \
-- \
<file1> <file2> ...
We generate a performance report with BazelCI. The generator script can be found
under the /report directory.
Example Usage: $ python3 report/generate_report.py --date=2019-01-01 --project=dummy --storage_bucket=dummy_bucket
For more detailed usage information, run: $ python3 report/generate_report.py --help
The tests for each module are found in the same directory. To run the test, simply:
$ bazel test ...