Skip to content

Commit 54418e3

Browse files
committed
runner: operator docs and the example campaign config
runner/README.md documents the CLI, the bundle layout contract, and the compatibility floor. The root README gets the end-to-end operator flow. example-campaign.toml is the annotated config operators copy.
1 parent 57548b9 commit 54418e3

3 files changed

Lines changed: 644 additions & 22 deletions

File tree

README.md

Lines changed: 169 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1212
The 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
1617
results 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
5152
errors 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.)
62178
gcloud 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.
77193
git add docs/runs
78194
git 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
124252
clear message until two repository variables exist
@@ -129,8 +257,9 @@ clear message until two repository variables exist
129257

130258
Creating 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
```
155284
stellar-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

Comments
 (0)