@@ -10,7 +10,8 @@ step, no server: the numbers live in git and the site is just HTML/JS reading th
1010## What this is
1111
1212The bench suite (` stellar-rpc bench-ingest cold|hot ` , ` bench-query cold|hot ` ) runs in
13- campaigns on an AWS NVMe devbox (` m6id.2xlarge ` ). Each campaign is several configurations
13+ campaigns on an AWS NVMe devbox (` m6id.2xlarge ` ), driven by the config-driven runner in
14+ [ ` runner/ ` ] ( runner/ ) (see "Run a campaign" below). Each campaign is several configurations
1415× 5 fresh-process runs; every run writes CSVs
1516(` stage,n,n_items,total_ns,p50_ns,p90_ns,p99_ns,max_ns ` ) into its own directory, and the
1617results are mirrored to GCS under ` gs://rpc-full-history/benchmarks/ ` .
@@ -51,14 +52,129 @@ To smoke-test the viewer headlessly (loads each run in a jsdom DOM, asserts zero
5152errors and the expected figure/section counts and sanity values), run ` make smoke `
5253(needs Node; installs ` jsdom ` under ` tests/smoke/ ` on first run).
5354
54- ## Add a run locally (the primary flow today)
55+ ## Run a campaign
5556
56- On a laptop that's authenticated to GCS (` gcloud auth login ` ), pull a results directory
57- down, convert it, and commit. Worked example:
57+ Campaigns run on the benchmark devbox via the campaign CLI in [ ` runner/ ` ] ( runner/ ) — this
58+ repo's operations side. The runner treats stellar-rpc as a ** black box** : it maintains a
59+ build clone of it under ` $BENCH_ROOT/src ` , builds the configured ref, and drives the
60+ bench subcommands — no standalone stellar-rpc checkout is needed anywhere. See
61+ [ runner/README.md] ( runner/README.md ) for the full CLI and config reference, the bundle
62+ layout it produces, and the minimum stellar-rpc ref it requires (the compatibility floor).
5863
5964``` bash
60- # 1. Pull the results directory from GCS. (This is the exact path recorded as this
61- # run's provenance in docs/runs/pubnet-2026-07-13.json.)
65+ # 0. One-time on a fresh devbox (and again after every instance stop/start,
66+ # which wipes the NVMe instance store): provision the machine.
67+ ./runner/bootstrap.sh
68+
69+ # Everything below runs from runner/ (the devbox has Go; bootstrap installs it).
70+ cd runner
71+
72+ # 1. Write a campaign config (copy example-campaign.toml, adjust the keys) and
73+ # sanity-check the full command plan. --dry-run builds, downloads, and runs
74+ # nothing — it works on any machine, e.g. a laptop:
75+ go run ./cmd/campaign run my-campaign.toml --dry-run
76+
77+ # 2. Run it (in tmux — campaigns run for hours). Results land in
78+ # $BENCH_ROOT/results/<name>-<sha>-<stamp>/, tarred to /tmp so the bundle
79+ # survives an instance stop.
80+ go run ./cmd/campaign run my-campaign.toml
81+
82+ # 3. Publish the bundle to GCS. This happens automatically when the config
83+ # sets publish_uri; run it by hand otherwise (or to retry a failed upload —
84+ # an upload that died partway leaves objects at the destination, so the
85+ # retry needs --force, which is a MERGE: files already there that this
86+ # bundle lacks survive it. Against a destination that really is empty,
87+ # --force only skips the emptiness check, so it is safe on a first publish):
88+ go run ./cmd/campaign publish /mnt/nvme/bench/results/< run-id> \
89+ gs://rpc-full-history/benchmarks --force
90+ ```
91+
92+ The published bundle is exactly what the next section ingests into a committed run
93+ JSON — closing the loop: campaign config → run → publish → ingest → viewer.
94+
95+ ## Add a run
96+
97+ ` scripts/ingest.sh ` is the one path from a published bundle to a committed run: it
98+ fetches the bundle, converts it, and stages the result as a ` run/<run_id> ` branch. The
99+ same script backs ` make ingest ` and the GitHub Action below, so a run ingested from a
100+ laptop and one ingested from CI are byte-identical, commit message included.
101+
102+ ``` bash
103+ # On a laptop authenticated to GCS (gcloud auth login). That gs:// path is the
104+ # one recorded as campaign.source_gcs in docs/runs/phase3-c6id8xl-c48a55c6-20260724T214257Z.json.
105+ make ingest \
106+ BUNDLE=gs://rpc-full-history/results/phase3-c6id8xl-c48a55c6-20260724T214257Z \
107+ KIND=synthetic
108+ ```
109+
110+ ` BUNDLE ` is auto-detected and may be a ` gs:// ` or ` s3:// ` bundle URI, a local bundle
111+ directory, or a ` bench-results-<run_id>.tgz ` tarball — the shapes the campaign CLI's
112+ ` run ` and ` publish ` leave behind. ` KIND ` is the one thing you have to state, because it is
113+ the one fact the bundle doesn't record: ` datasets[].kind ` in the manifest is the dataset's
114+ * transport* (` packs-gs ` , ` bsb-s3 ` , …), not pubnet-vs-synthetic.
115+
116+ Everything else is derived from the bundle's own ` metadata.json `
117+ (see [ SCHEMA.md § Inputs] ( SCHEMA.md#inputs--result-bundle-layouts--manifests ) ):
118+
119+ | Derived | From |
120+ | -------------------------| --------------------------------------------------|
121+ | Run id, and so the file | ` run_id ` → ` docs/runs/<run_id>.json ` |
122+ | Run date | ` started_at ` |
123+ | Run name | ` campaign.name ` |
124+ | ` campaign.source_gcs ` | the ` gs:// ` URI you passed (recorded as provenance) |
125+ | Commit / PR body | campaign config, close interval, datasets, hardware |
126+
127+ Three modes, least to most committal:
128+
129+ | Mode | Effect |
130+ | -------------| -----------------------------------------------------------------------------|
131+ | ` --dry-run ` | Converts into a ** temp** directory — never ` docs/runs/ ` — and prints the converter output, the derived run id, the would-be commit body, the would-be branch, and the exact git/gh commands full mode would run. Executes none of them. A remote bundle isn't fetched either; the fetch command is printed instead, so a dry run works offline. |
132+ | ` --local ` | Converts into ` docs/runs/ ` , creates ` run/<run_id> ` off HEAD, commits the two changed files. No push, no PR. |
133+ | (default) | ` --local ` , then ` git push -u origin run/<run_id> ` and ` gh pr create ` . |
134+
135+ ` make ingest ` passes ` --local ` on purpose: it stops at the commit, so you can read the
136+ diff and ` make serve ` the result before anything leaves the machine. Call the script
137+ directly for the other two modes:
138+
139+ ``` bash
140+ # Look before you leap — converts to a temp dir, touches nothing:
141+ scripts/ingest.sh gs://rpc-full-history/results/< run-id> --dataset-kind synthetic --dry-run
142+
143+ # Full: convert, branch, commit, push, open the PR (needs gh authenticated):
144+ scripts/ingest.sh gs://rpc-full-history/results/< run-id> --dataset-kind synthetic
145+ ```
146+
147+ The PR targets the default branch and carries the campaign one-liners, any converter
148+ warnings, and a note pointing the reviewer at the preview: ` pr-preview.yml ` publishes
149+ that PR's ` docs/ ` , so the new run can be read in the viewer before anyone merges it.
150+ Merging is the deploy.
151+
152+ Two rails keep the flow from surprising you. The script refuses to run against a working
153+ tree with uncommitted tracked changes, and refuses to overwrite an already-ingested run —
154+ an existing ` docs/runs/<run_id>.json ` is an error until you pass ` --force ` . A ` --force `
155+ re-ingest reuses the existing ` run/<run_id> ` branch instead of failing on it, and exits
156+ quietly when the reconverted JSON turns out byte-identical to the committed one.
157+
158+ Anything after a literal ` -- ` is passed straight through to ` convert.py ` , which is how you
159+ override a derived field without leaving the flow:
160+
161+ ``` bash
162+ scripts/ingest.sh gs://rpc-full-history/results/< run-id> --dataset-kind synthetic -- \
163+ --run-name " Phase 3 — c6id.8xlarge rerun" \
164+ --unit-facts converter/facts/synthetic-2026-07-15.json
165+ ```
166+
167+ ### ` make convert ` — the layer underneath
168+
169+ ` make convert ` calls ` converter/convert.py ` and nothing else: no fetch, no branch, no
170+ commit. Reach for it when the bundle can't identify itself — the ** legacy** pubnet and
171+ synthetic layouts predate ` metadata.json ` , so ` --run-id ` /` --run-name ` /` --run-date ` have no
172+ defaults to fall back on — or when you want to name every field by hand. Worked example
173+ against the archived pubnet run (` docs/runs/archive/pubnet-2026-07-13.json ` ):
174+
175+ ``` bash
176+ # 1. Pull the results directory from GCS. (This is the exact path recorded as that
177+ # run's provenance.)
62178gcloud storage cp -r \
63179 gs://rpc-full-history/benchmarks/2026-07-13-user-dev-063a \
64180 ./results-in
@@ -72,11 +188,10 @@ make convert \
72188 RUN_DATE=2026-07-13 \
73189 GCS=gs://rpc-full-history/benchmarks/2026-07-13-user-dev-063a
74190
75- # 3. Review the diff, then commit + push. The deploy-pages workflow syncs
76- # docs/ to the gh-pages branch and Pages redeploys .
191+ # 3. Review the diff, then commit on a branch and open a PR — the same place
192+ # `scripts/ingest.sh` would have left you .
77193git add docs/runs
78194git commit -m " Add run pubnet-2026-07-13"
79- git push
80195```
81196
82197` make convert ` variables:
@@ -112,13 +227,26 @@ GCS path, summary paths).
112227
113228## GitHub Action flow (` .github/workflows/ingest.yml ` )
114229
115- ` workflow_dispatch ` with inputs ` gcs_path ` , ` run_id ` , ` run_name ` , ` dataset_kind `
116- (pubnet|synthetic), ` run_date ` , and optional ` unit_facts ` (a repo path to a ` --unit-facts `
117- sidecar JSON for synthetic dataset meta). It checks out the repo, authenticates to GCP via Workload
118- Identity Federation, ` gcloud storage cp -r ` the results directory into ` ./results-in ` , runs
119- the converter, and commits the new/updated ` docs/runs/*.json ` + manifest back to ` main ` .
120- Permissions are minimal: ` contents: write ` (to commit) and ` id-token: write ` (for the OIDC
121- token WIF exchanges).
230+ ` workflow_dispatch ` running ` scripts/ingest.sh ` in full mode — the same script as the
231+ local flow above, which is the point: CI is not a second implementation that can drift.
232+ Three inputs, two of them required:
233+
234+ | Input | Required | Meaning |
235+ | ----------------| ----------| ---------------------------------------------------------------|
236+ | ` gcs_path ` | yes | ` gs:// ` path to the campaign result bundle |
237+ | ` dataset_kind ` | yes | ` pubnet ` or ` synthetic ` |
238+ | ` extra_args ` | no | Passed to ` convert.py ` after ` -- ` (e.g. ` --unit-facts … ` ) |
239+
240+ Run id, date, and name are not asked for — they come from the bundle's ` metadata.json ` .
241+ ` extra_args ` is one freeform string word-split on whitespace, so a value containing spaces
242+ can't be expressed there; use the local flow for those.
243+
244+ The job checks out the repo, authenticates to GCP via Workload Identity Federation, then
245+ hands the ` gs:// ` path to the script, which fetches, converts, commits on ` run/<run_id> ` ,
246+ pushes, and opens the PR against the default branch. Runs therefore arrive as reviewable
247+ PRs with a ` pr-preview.yml ` render attached, exactly like the local flow — not as a push
248+ straight to ` main ` . Permissions: ` contents: write ` and ` pull-requests: write ` (push the
249+ branch, open the PR) plus ` id-token: write ` (the OIDC token WIF exchanges).
122250
123251** This workflow does not run yet — the GCP-side setup is pending.** It fails early with a
124252clear message until two repository variables exist
@@ -129,8 +257,9 @@ clear message until two repository variables exist
129257
130258Creating them requires, in GCP project ** ` dev-hubble ` ** , a workload identity pool + provider
131259(federating this GitHub repo) and a service account with ` roles/storage.objectViewer ` on
132- ` gs://rpc-full-history ` . That GCP setup is out of this repo's hands; until it lands, use the
133- local flow above.
260+ ` gs://rpc-full-history ` . That GCP setup is out of this repo's hands; until it lands,
261+ ` make ingest ` from a laptop is the way runs get in — and it runs the same script, so
262+ nothing about a run changes when the dispatch starts working.
134263
135264## Data model
136265
@@ -153,12 +282,23 @@ statement.
153282
154283```
155284stellar-rpc-benchmarks/
156- ├── Makefile # convert / test / serve / help
285+ ├── Makefile # ingest / convert / test / smoke / serve / help
157286├── README.md
158287├── SCHEMA.md # run JSON schema v1 (the data contract)
159288├── .github/
160289│ └── workflows/
161- │ └── ingest.yml # workflow_dispatch: GCS results dir → committed run
290+ │ ├── ingest.yml # workflow_dispatch: GCS bundle → run PR (delegates to scripts/ingest.sh)
291+ │ ├── deploy-pages.yml # sync main:/docs to the gh-pages branch Pages serves
292+ │ ├── pr-preview.yml # publish each PR's docs/ under gh-pages:/pr-preview/pr-<n>/
293+ │ ├── runner-go.yml # go vet + go test for the campaign runner
294+ │ └── shellcheck.yml # lint the remaining shell scripts on every PR that touches them
295+ ├── runner/ # benchmark operations: the devbox side producing result bundles
296+ │ ├── bootstrap.sh # provision the devbox (idempotent, no builds)
297+ │ ├── cmd/campaign/ # campaign CLI: run · plan · preflight · publish (see runner/README.md)
298+ │ ├── internal/ # config · plan · run · preflight · publish · bundle
299+ │ └── example-campaign.toml # annotated config to copy from
300+ ├── scripts/
301+ │ └── ingest.sh # bundle → converted run → run/<id> branch → PR (make ingest)
162302├── converter/
163303│ ├── convert.py # results dir → docs/runs/<id>.json (+ manifest), stdlib only
164304│ ├── facts/ # per-unit sidecar facts (e.g. synthetic model/tps/pack)
@@ -168,10 +308,17 @@ stellar-rpc-benchmarks/
168308└── docs/ # GitHub Pages root (static vanilla-JS viewer)
169309 ├── index.html # the viewer shell (dropdown / ?run=<id>)
170310 ├── app.js # renderers (per dataset.kind) + charts
171- ├── styles.css # design system (light + dark)
311+ ├── styles.css # design system (light + dark), shared by every page
312+ ├── summary.html # stakeholder summary page (summary.js + summary.css)
313+ ├── latency-model.html # end-to-end latency model against the phase targets
314+ ├── tx-submission.html # transaction-submission report (txsub.js)
315+ ├── targets.json # Phase 1/2/3 performance targets — single source of truth
316+ ├── dataset-sizes.json # measured sizes of the synthetic dataset profiles
317+ ├── txsub/ # tx-submission harvest summaries, verbatim (+ index.json)
172318 └── runs/
173319 ├── index.json # manifest of runs (oldest date first)
174- └── <run-id>.json # one file per run (schema v1)
320+ ├── <run-id>.json # one file per run (schema v1)
321+ └── archive/ # retired pre-campaign runs, with their own manifest
175322```
176323
177324## Future work
0 commit comments