This document is the reference for the operator: the requirements, every make target and knob, the run files, the remote state, the CI bootstrap, and the owner steps. README.md gives the overview, and METHOD.md gives the measurement method.
- Bash, GNU Make, Git,
curl,tar, and an OpenSSH client - Python 3.11 or later. The rig uses only the Python standard library.
- Terraform 1.10 or later
- AWS CLI v2 with an active session for the default profile
Start the AWS session before you create the rig:
aws sso loginBench the nightly build of a commit on the rapira main branch against a base build:
make up NIGHTLY=<sha7> BASE=<sha7>
make bench
make downBench a Git ref that the server builds from source against a base build:
make up REF=pr/97 BASE=<sha7>
make bench
make downA build of REF or of make sync links the PHP of the Fedora php-embedded package. The base build loads the libphp of its nightly tarball. The deltas of such a run therefore include the difference between the two PHP builds. For an A/B of two rapira builds, use NIGHTLY.
make up checks the vCPU quota, writes terraform/rig.auto.tfvars, applies the Terraform stack, and runs make provision. make bench runs the suite, writes the run file, and prints the report. make down destroys the rig.
Run make pgo after make bench to make a PGO profile of the new build. The server builds the commit of the new build with instrumentation (box/pgo.sh). Each target of the suite then runs the instrumented build for one rate stage with its rate, and the server merges the counters. make pgo copies merged.profdata and pgo.json to pgo/. pgo.json records the commit, the rustc and llvm-profdata versions, and the rustflags of the instrumented build and of a build that reads the profile. A profile fits only the build of its commit. The pass fails when the profile has no request counts of the workers. A make sync build has no commit, so make pgo needs a NIGHTLY or REF rig. The instrumented build is slower, so make pgo records no numbers.
NIGHTLY is the first 7 characters of the commit SHA of a nightly release asset of rapira-rs/rapira. The core Nightly workflow deletes the assets of older builds, so use the SHA of the current nightly release. REF accepts a branch, a tag, a commit, or pr/N. When you set NIGHTLY, provisioning ignores REF. The new build of the run is the build of NIGHTLY or REF.
NIGHTLY_DIR selects a captured nightly directory with its tarball, SHA256SUMS file, and nightly.json. The JSON records sha as the full commit SHA, plus version, asset, and sums as the release version and file names. Set NIGHTLY to that commit prefix. Keep the directory outside the checkout. Provisioning copies it to the server, checks the checksum, and installs those bytes without another nightly release lookup. CI uses this path so a moved tag or a rebuild cannot change the measured binary.
BASE is the first 7 characters of the commit SHA of the base build. It is required. The server installs the base build from the release cache, so the tarball of BASE must be in the binaries release of this repository. Set BASE equal to NIGHTLY for an A/A run, which measures the noise of the rig. This command lists the cached tarballs:
gh release view binaries -R rapira-rs/benchmarks --json assets --jq '.assets[].name'python3 -m rig base --index <file> --new <sha7> prints the base build that CI selects: the sha7 of the newest non-smoke run in the index file whose sha is not <sha7>. It fails when the index has no such run. --base <sha7> prints the given sha7. This command gets the index of the board:
gh api -H "Accept: application/vnd.github.raw+json" "repos/rapira-rs/benchmarks/contents/data/index.json?ref=gh-pages" > index.jsonProvisioning installs only the apps that the suite uses. Set the same SUITE on make up and on make bench.
| Instance | Condition | Software |
|---|---|---|
| Server | All runs | PHP with opcache, the shared servers/php.ini, and two rapira binaries: the new build under /opt/bench/rapira/<sha7>, from the nightly release asset or a build of REF, and the base build under /opt/bench/rapira/base-<sha7>, from the binaries release |
| Server | gRPC target | Composer and the pure PHP protobuf runtime of apps/grpc/composer.lock |
| Loader | All runs | wrk2 at commit 44a94c1, h2load from the Fedora nghttp2 package, and a chrony synchronization check |
The Fedora 44 EC2 image supplies Bash, dnf, sudo, the OpenSSH server, cloud-init, systemd, core utilities, and the procps and iproute tools. Provisioning uses these tools but does not install them.
suites/targets.toml defines every target. A target name is <app>-rapira-<mode>, with the suffix -static for the static middleware; the gRPC target is grpc-rapira. A suite file has these keys:
name,rounds,smoke, andtargets, the list of target names.[connections]: the connection count of each proto,http1andgrpc.[grpc]:streams, the stream count of each gRPC connection.[stages.rate]:warmup_sandduration_sof the rate stage. The rate stage runs one rapira process per server vCPU.[stages.cap]:warmup_s,duration_s, andprocesses, the rapira process count of the capacity stage.[stages.rate.rates]and[stages.cap.rates]: the rate of each target of the suite in that stage kind, as the total req/s over all loaders. The keys are target names.
ci is the per-merge suite: the five targets, 3 rounds, 1000 connections for the HTTP targets, and 100 connections with 100 streams each for the gRPC target. The rate stage has an 11 s warm-up and 15 s measured. The capacity stage has a 5 s warm-up, 15 s measured, and 2 processes. METHOD.md gives the rules for its rates.
The driver refuses a suite before it creates a run directory when one of these conditions is true:
roundsis less than 1.stagesdoes not have exactly the keysrateandcap.- A
duration_sis less than 15, or awarmup_sis negative. stages.ratesetsprocesses, orstages.cap.processesis missing or less than 1.- A target of the suite has no rate in a stage kind, or a rates table names a target that is not in the suite.
- A target name is not in
suites/targets.tomlor is in the list two times, or a proto of a target has no connection count. - A rate or a connection count is not a multiple of the loader count.
make bench also refuses an HTTP connection count that is not a multiple of the loader count times the loader vCPU count. It does this check after the rig is up.
SUITEselectssuites/<name>.toml. It defaults toci.NIGHTLYselects the nightly release asset by its SHA prefix.NIGHTLY_DIRsupplies a captured nightly directory forNIGHTLY. The CLI option is--nightly-dir.REFselects the Git ref that the server builds.BASEselects the base build by its SHA prefix. It is required.ROUNDSreplaces the round count of the suite. It must be at least 1.SERVER_TYPEdefaults toc7a.2xlarge.LOADER_TYPEdefaults toc7a.xlarge.LOADER_COUNTdefaults to 2. The default rig needs 16 vCPUs of the quota.LOADER_COUNTmust divide every rate and connection count of the suite, and the HTTP connection count must be a multiple of the loader count times the loader vCPU count.make benchrefuses another count after the rig is up.AZdefaults toeu-central-1a.REGIONdefaults toeu-central-1.AMIpins an AMI. Without it, Terraform selects the newest Fedora 44 image, which Fedora rebuilds every day. Pin it when a result set takes more than one day.TTLsets the instance lifetime in minutes. It defaults to 60.TF_BACKEND=s3keeps the Terraform state in S3. The defaultlocalkeeps the state interraform/.FRAME_POINTERS=1builds rapira with frame pointers for a perf session. It applies to server builds only.RUNselects the run directory ofmake report.PAGESselects the pages directory ofmake board. It defaults toruns/pages.AUTO_EXTEND=0stops a run when the remaining instance lifetime is too short.
make provisionprovisions the rig again, for example after a change ofSUITEorREF. It needsBASEandNIGHTLYorREF, asmake updoes.make statusshows the Terraform outputs, the instances, and the remaining lifetime of each box.make extend TTL=<minutes>sets a new lifetime on every box.make syncbuilds the local../coreworking tree on the server and installs it under/opt/bench/rapira/local. It extends the lifetime of every box before the build when needed. The nextmake benchuses that binary as the new build. The base build does not change. The rig must come frommake up REF=<ref> BASE=<sha7>, because a nightly rig has no Rust toolchain. A build ofREFor ofmake synclinks the PHP of the Fedoraphp-embeddedpackage. The base build loads the libphp of its nightly tarball. The deltas of such a run therefore include the difference between the two PHP builds. For an A/B of two rapira builds, useNIGHTLY.make reportprints the summary table of the newest run.make report RUN=runs/<id>prints another run.make boardcopies the board files intoPAGESand serves the directory on 127.0.0.1:8000. Fill the directory first withpython3 -m rig publish --pages-dir runs/pages runs/<id>/run.json.make lockcreatesapps/grpc/composer.lockfromapps/grpc/composer.json. It needs PHP 8.5 and Composer on the operator machine.make testruns the unit tests.make nukeremoves the tagged AWS resources when the Terraform state is not usable.make grpc_fixturesbuilds the gRPC descriptor, the PHP classes, and the request and response fixtures. It needs Go and access to the buf remote plugins.
Each run writes runs/<id>/. The run id is <UTC timestamp>-<suite>-<rapira sha7>. The directory holds:
run.json: the run file with the schemarapira-bench-run/3. It records the rig, the new build inrapirawith its pull request, the base build inbase, the version lines of the server, the php.ini text, the app hashes, the loaders, the suite, every cell, the run status, and the summary. A cell records its build, its stage kind, its process count, its rate, its achieved rate, its latency, its RSS, itsheldstate, its flags, and the record of each loader with the wrk2 calibration values.raw/<cell>/: the wrk2 or h2load output of each loader with itsRESULTline, the rendered rapira config, the WARN and ERROR lines of the server log, and the snapshots. A failed start also records its diagnostic instart.txt.
The run status is complete when every planned cell ran. A complete run can hold void cells. The status is incomplete when a cell is missing or interrupted, and broken when every new cell of one target is void. make bench and make report return a nonzero status when the run is not complete.
python3 -m rig publish --pages-dir <dir> runs/<id>/run.json adds a complete run to a checkout of the gh-pages branch: it writes data/<id>.json and updates data/index.json. The index entry records the run schema and the sha of the base build. rig publish refuses an incomplete or a broken run.
python3 -m rig bench --suite <name> --smoke marks the run as a smoke run. The board does not show it, and the CI commit check ignores it.
Each run compares its two builds on the same instances. The same build gives different numbers in two runs. Read METHOD.md before you publish a number.
The board uses the first non-smoke run of the current method as its fixed chart baseline. It compares the new median of each run with the new median of that run. The run file and make report show the comparison of the two builds in a run.
The release with the tag binaries in this repository keeps the php8.5 linux x86_64 nightly tarballs of rapira and the rapira-v<version>-SHA256SUMS.txt files of the core nightly release, under their core names. The base build of a run comes from this release, because the core Nightly workflow deletes the assets of older builds.
- Provisioning finds the tarball of
BASEin this release, verifies it against the cached SHA256SUMS file, and checks its libraries withldd. - The publish job of CI uploads the tarball and the SHA256SUMS file of each new build before the publish. After the publish, it deletes all tarballs except the newest 10 with their SHA256SUMS files.
- A
BASEthat is not in the release fails the provisioning. In CI it fails the bench job beforemake up, with an error that names the sha.
make up TF_BACKEND=s3 copies terraform/backend.tf.s3 to terraform/backend.tf. terraform/s3.tfbackend holds the key, the region, and use_lockfile. Only the bucket comes from the TF_CLI_ARGS_init environment variable:
export TF_CLI_ARGS_init='-backend-config=s3.tfbackend -backend-config=bucket=<bucket>'
make up TF_BACKEND=s3 NIGHTLY=<sha7> BASE=<sha7>Keep TF_BACKEND=s3 and the variable set for every later target that uses Terraform, for example make down.
The instances use on-demand billing per second. In eu-central-1 the default rig costs 0.93704 USD per hour: 0.46852 USD for the c7a.2xlarge server and 0.23426 USD for each c7a.xlarge loader.
The ci suite has 60 cells. The measured overhead is about 5.7 s per cell, so a rate cell takes about 32 s and a capacity cell about 26 s. The estimated cell time is about 29 minutes. The rig creation, the provisioning, and the destroy take about 2.0 minutes. Without the PGO step, a CI run therefore holds the rig for about 31 minutes, which costs about 0.48 USD. The EBS volumes and the public IPv4 addresses add about 0.02 USD. With cells 20% longer, the estimated time is about 36 minutes, which costs about 0.57 USD. The PGO step holds the rig longer for the instrumented build, which is not measured yet, and one rate stage of about 32 s for each of the 5 targets. The job timeout of 90 minutes stops a run that hangs.
Every box has a lifetime. The bootstrap sets 75 minutes. Provisioning replaces it with TTL. make bench extends it from the run estimate: for each cell, warmup_s plus duration_s of its stage kind plus 20 seconds, and 300 seconds for the run. For the ci suite that is 30 x (11 + 15 + 20) + 30 x (5 + 15 + 20) + 300 = 2880 seconds. When the lifetime ends, the box shuts down, and a shutdown terminates the instance. A CI runner that stops before the provisioning therefore bills at most about 1.2 USD.
- Run
make downafter every session. Checkmake statuswhen you are not sure. - When
make downfails, run it again. Placement group deletion can lag instance termination. - When the Terraform state is lost, run
make nuke, thenmake up.make nukedeletes only the resources with the tagProject=rapira-bench.
make test runs the unit tests with python3 -m unittest discover -s tests -t .. tests/test_box.py runs the box scripts in a container. The header of that file gives the command.
Run node --test tests/test_board_browser.cjs for browser checks. They require Playwright Core and Chromium. Set PLAYWRIGHT_CORE and CHROMIUM to select local installations.
The stack in terraform/ci/ creates the AWS resources of the bench workflow: the GitHub OIDC identity provider, the IAM role rapira-bench-ci, and the S3 bucket rapira-bench-tfstate-<account id> for the rig state. The owner applies it once with the default AWS CLI profile. Its state stays in terraform/ci/ on the owner machine and git ignores it. Keep that state file: a later apply needs it to find the resources.
aws sso login
terraform -chdir=terraform/ci init
terraform -chdir=terraform/ci apply
terraform -chdir=terraform/ci outputThe output role_arn is the value of the repository secret AWS_ROLE_ARN. The output bucket is the value of the repository secret TF_STATE_BUCKET. Secrets are masked in the workflow logs. Repository variables are not.
The role trusts only jobs on the main branch of this repository. This repository uses the immutable OIDC subject format, which contains the owner id and the repository id. The variable github_sub_prefix holds that prefix. This command prints the current value:
gh api repos/rapira-rs/benchmarks/actions/oidc/customization/sub --jq .sub_claim_prefixAn AWS account has at most one OIDC provider for token.actions.githubusercontent.com. If the apply stops with EntityAlreadyExists, import the provider. Then apply again.
terraform -chdir=terraform/ci import aws_iam_openid_connect_provider.github arn:aws:iam::<account id>:oidc-provider/token.actions.githubusercontent.comAfter the import, this stack owns the provider. A destroy of this stack removes the provider for every role that uses it.
The role policy allows the EC2 actions of the rig stack in eu-central-1, all EC2 describe calls, the service quota read, and read and write access to the rig/ objects of the state bucket.
With TF_BACKEND=s3, the rig stack keeps its state in the bucket. terraform/s3.tfbackend holds the key rig/terraform.tfstate, the region, and use_lockfile = true. The bucket name holds the account id, so it is not in the repository: the CI workflow gives it to terraform init through TF_CLI_ARGS_init. If a CI run leaves a rig that bills, run make nuke on the operator machine. It finds the resources by their tag and needs no state.
After each successful Nightly run on the rapira main branch, the core repository starts .github/workflows/bench.yml in this repository. The bench job runs these steps:
- The resolve step finds the commit of the current
nightlyrelease, looks up the merged pull request of that commit for the board label, and stops when the board already has that commit. It selects the base build with thebaseinput of the dispatch, or else withpython3 -m rig baseon the index of the board. It fails when the index has no other non-smoke run and thebaseinput is empty. It checks that the tarball of the base build is in thebinariesrelease. It downloads the new tarball and its SHA256SUMS file into$RUNNER_TEMP, records the full commit and asset names innightly.json, and keeps that directory as an artifact for 1 day. - The rig step creates the rig with the S3 backend and provisions it with
NIGHTLY,NIGHTLY_DIR, andBASE. It copies the captured nightly directory to the server only. A capacity error retries once ineu-central-1bwith the same directory. - The suite step runs
suites/ci.toml. It fails when the run is incomplete or broken. - The job uploads
run.jsonandraw/as artifacts for 90 days. - The PGO step runs
python3 -m rig pgo --suite ciand uploadspgo/as the artifactpgo-<sha7>for 90 days. The publish job does not wait for it, so a failed PGO step keeps the bench job red and the run still publishes. make downruns at the end of each bench job that passes the commit check, also after a failure.make nukeruns only whenmake downfailed.
The bench job sets its ready output only when the suite and all artifact uploads succeed, and cleanup succeeds through make down or its make nuke fallback. A failed make down keeps the bench job red. A successful fallback still permits publication. A cancelled workflow does not publish.
The publish job uploads the captured tarball and its SHA256SUMS file to the binaries release, adds the run to the gh-pages branch, copies board/ there, and pushes the branch. It uploads the site with actions/upload-pages-artifact and deploys it with actions/deploy-pages. The github-pages environment and the pages: write and id-token: write permissions enable that deployment. See GitHub Pages custom workflows. Then it deletes all tarballs except the newest 10 with their SHA256SUMS files. A failed delete does not stop the publish. The commit step skips when nothing changed, and each attempt uploads the site under its own artifact name, so a rerun of the publish job works.
To choose the base build of a manual run, give the base input:
gh workflow run bench.yml -R rapira-rs/benchmarks -f base=<sha7>Only one bench run runs at a time. A new dispatch replaces a waiting one and never stops a running one. The bench job stops after 90 minutes. The rig step stops after 30 minutes, the suite step after 55 minutes, and the PGO step after 30 minutes. A local rig and the CI bench cannot run at the same time, because they use the same resource names and the same tag. Check the Bench runs of this repository before make up and before make nuke.
Do these owner steps once, in this order:
- Examine the tracked files and the git history for account ids, IP addresses, and keys. Then make this repository public.
- Create the
gh-pagesbranch with the commands below. - In the repository settings, open Pages. Select "GitHub Actions" as the source. The workflow keeps the board history on
gh-pagesand deploys the site through the Pages actions. - Apply
terraform/cias the "CI bootstrap" section above shows. - Set the repository secrets
AWS_ROLE_ARNandTF_STATE_BUCKETwith the commands below. - Create a fine-grained token for the resource owner
rapira-rswith access to the repositoryrapira-rs/benchmarksonly and the permission "Actions: Read and write". If the organization approves tokens, approve the request. The token has an expiry date. Create a new token before that date and repeat step 7. - Store the token as the secret
BENCH_DISPATCH_TOKENinrapira-rs/rapira. - Copy
docs/core-dispatch.ymlto.github/workflows/bench-dispatch.ymlinrapira-rs/rapirathrough a pull request. - Create the
binariesrelease with the commands below. Upload the tarball and the SHA256SUMS file of the current nightly build from thenightlyrelease ofrapira-rs/rapira. This build is the base build of the first run. - Start the first run by hand with
gh workflow run bench.yml -R rapira-rs/benchmarks -f base=<sha7>, where<sha7>is the build of step 9. Examine the result on the board.
Commands for step 2:
git switch --orphan gh-pages
git commit --allow-empty -s -S -m "chore: start the board branch"
git push origin gh-pages
git switch mainCommands for step 5 and step 7:
gh secret set AWS_ROLE_ARN -R rapira-rs/benchmarks --body "$(terraform -chdir=terraform/ci output -raw role_arn)"
gh secret set TF_STATE_BUCKET -R rapira-rs/benchmarks --body "$(terraform -chdir=terraform/ci output -raw bucket)"
gh secret set BENCH_DISPATCH_TOKEN -R rapira-rs/rapiraCommands for step 9:
gh release download nightly -R rapira-rs/rapira -p 'rapira-v*-nightly.<sha7>-php8.5-linux-x86_64.tar.gz' -p 'rapira-v*-nightly.<sha7>-SHA256SUMS.txt'
gh release create binaries -R rapira-rs/benchmarks --title binaries --notes "The rapira builds of the board runs."
gh release upload binaries -R rapira-rs/benchmarks rapira-v*-nightly.<sha7>-*