perf(upload): skip suite objects the bucket already holds - #302
Conversation
A suite directory is content-addressed: the hash is a digest of its pre-run and test step file contents, so a given key under suites/<hash>/ always holds the same bytes. UploadSuiteDir re-sent all of them on every completed run. For the jochemnet bloatnet suite that is ~12.4 GB a run — a 9.4 GiB pre-run bundle plus 2.96 GB of fixtures — and that suite alone completes 12.8 runs a day, so roughly 158 GB/day of PUTs that overwrite identical objects. It only went unnoticed because the pre-run bundle was failing with EntityTooLarge before it could be sent twice. List the destination prefix once (3 requests for 2733 keys, versus a HEAD per file) and skip anything already there at the same size. Where the ETag is a plain MD5 — objects small enough to have gone up in a single part — verify that too, so the decision is content-based rather than size-based for all but the one file whose identity the suite hash already pins. A multipart ETag is a digest of part digests, so checking it would mean re-reading the whole bundle, the exact cost this skip exists to avoid. summary.json is never skipped: metadata labels can change between runs without affecting the suite hash, which is why CreateSuiteOutput rewrites it every time.
The ETag check was there to catch same-size-different-bytes. With the pre-run cap in #304 every remaining suite file is under the part size, so that branch covers all of them — meaning a full read and MD5 of the whole suite, 1-3 GB, on every run, to avoid re-sending the same 1-3 GB. It was insurance against something the design already rules out: a suite hash is a digest of exactly these step files, and only benchmarkoor writes under that prefix, so a key cannot hold different bytes at the same size without the hash changing too. Drop it and compare size, which the listing already gives us for free. summary.json stays exempt. It is the one file that is rewritten rather than content-addressed, so a label edit that happens to preserve its length is plausible in a way it is not for the rest. Net 63 lines and one crypto import gone.
|
Simplified — the ETag check is gone, size alone now. It existed to catch same-size-different-bytes. With the pre-run cap in #304 every remaining suite file is under the 64 MiB part size, so that branch would have covered all of them: a full read and MD5 of the whole suite, 1–3 GB, on every run, to avoid re-sending the same 1–3 GB. And it guarded something the design already rules out — a suite hash is a digest of exactly these step files, and only benchmarkoor writes under that prefix, so a key cannot hold different bytes at the same size without the hash changing too. The skip is now one
Why this is still worth having alongside #304#304 removes the 9.4 GiB pre-run bundle, but only one of the five active suites has one. The other four re-send their full fixture payload on every run and #304 does nothing for them. Measured from
~158 GB/day and ~273k PUTs/day — about 8.2M class-A ops a month. #304 removes ~52 GB/day on top of that, all of it from the one suite. |
Implements @skylenet's suggestion: a size limit on pre-run bundles, 512MB default, anything over it skipped. ## Why The jochemnet bloatnet pre-run bundle is a single **9.4 GiB** `pre-run.request` — roughly 8k blocks of setup. `CreateSuiteOutput` copies it into the suite directory on every job, and the runner then uploads it. Nothing consumes it: the runner replays pre-runs from the fixtures cache, not from the suite, and the UI has no use for a bundle that size. ## What it does A pre-run step over `runner.benchmark.tests.max_pre_run_step_size` (default `512MB`) is recorded in `summary.json` and not copied, so it never reaches the bucket. `0` disables the limit. ```json "pre_run_steps": [ { "og_path": "pre_run/pre-run.request", "size_bytes": 10062313486, "omitted": true } ] ``` Two details worth flagging for review: **Checked with a `stat` before the copy, not at upload time.** Skipping it at the upload layer would still leave a 9.4 GiB write into `RUNNER_TEMP` on every job. Doing it here costs neither the write nor the transfer, and keeps the uploader generic — it has no business knowing which files are pre-run steps. **The entry is recorded as `omitted`, not dropped.** The suite stays honest about what the run replayed, and the UI gets something to check before offering the file for viewing — `TestFilesList.tsx:147` currently links `suites/<hash>/<og_path>/pre_run.request` unconditionally, which would 404 for an omitted bundle. That UI guard is a separate change; this PR gives it the flag to read. ## Tests - `TestCreateSuiteOutput_OmitsOversizedPreRunSteps` — over the limit: no file written, summary records `omitted` and the true size, test steps unaffected. - `TestCreateSuiteOutput_KeepsPreRunStepsWithinLimit` — under the limit and with the limit disabled, the bundle is stored exactly as before. ## How this relates to the other PRs - **#301** (merged) makes the upload of any large file work at all. Still wanted — it is what stops a >5 GiB file failing the whole suite upload, including the `summary.json` that the UI actually needs — but with this cap the 9.4 GiB path stops being exercised. - **#302** (skip unchanged objects) is orthogonal: it stops re-sending the ~2.96 GB of fixture payloads on every run, which this PR does not touch. Together the steady-state suite upload goes from ~12.4 GB a run to ~5 MB. - **#303** (merge the stored summary) is a different bug and independent of both. Note it also adds a parameter to `CreateSuiteOutput`, so whichever of #303/#304 merges second needs a trivial rebase — happy to fold both into an options struct if you'd rather. ## Verification `make build-core`, `make lint-core` (0 issues), full `go test ./pkg/... ./cmd/...` all pass locally.
Follow-up to #301. That PR made the suite upload succeed; this one stops it re-sending the same bytes on every run.
The waste
A suite directory is content-addressed —
ComputeSuiteHashdigests the pre-run and test step file contents — so a given key undersuites/<hash>/always holds the same bytes.UploadSuiteDirre-sent all of them on every completed run.For
0d93b5bf3b970403(jochemnet stateful) that is ~12.4 GB per run: a 9.4 GiB pre-run bundle (pre-run.request, bloatnet setup withBLOATNET_RECEIVER_CONTRACT_COUNT=100000) plus 2.96 GB of fixtures across 2733 files. That suite alone has completed 12.8 runs/day over the last two days, so roughly 158 GB/day of PUTs overwriting byte-identical objects — and every other suite pays the same tax in proportion to its size.It went unnoticed because the bundle was failing with
EntityTooLargebefore it could ever be sent a second time. #301 fixed the failure, which turns the waste on.The change
ListObjectsV2covers 2733 keys in 3 requests, versus aHEADper file.summary.jsonis never skipped. Metadata labels can change between runs without affecting the suite hash, which is exactly whyCreateSuiteOutputrewrites it every time.On ETags
Worth stating the limit plainly: an ETag is only an MD5 for single-part objects. For a multipart upload it is a digest of the part digests with a
-Nsuffix, and reproducing it means re-reading the whole file with the same part size — the exact I/O this skip avoids, on the exact file that motivated it. So the 9.4 GiB bundle is matched on size alone, which is sound here because its key is content-addressed. Everything else gets the stronger check.Tests
TestUploadSuiteDirSkipsUnchangedObjectsuploads a suite twice against a fake S3 that servesListObjectsV2with real sizes and MD5 ETags:summary.jsonVerification
make build-core,make lint-core(0 issues), fullgo test ./pkg/... ./cmd/...all pass locally.