benchcheck: parse go test -json instead of scanning text#516
Conversation
Replace the prefix-matching failure scanner with structured test2json parsing. benchcheck check now consumes 'go test -json' output: it detects build errors, failed tests, and crashes from 'fail' events (including the failing scope's output for context) instead of matching line prefixes, and reconstructs the plain benchmark text from the output events for benchfmt and benchstat. The workflow's bench steps now emit -json to base.json/head.json, and check writes the reconstructed text via -o-base-text/-o-head-text for benchstat to consume.
There was a problem hiding this comment.
Pull request overview
This PR makes benchcheck consume go test -json (test2json) output instead of scanning plain text, enabling structured failure detection and reconstruction of benchmark text for regression analysis and benchstat.
Changes:
- Added
parseTestJSONto parse test2json streams, extract failures fromfailevents, and reconstruct benchstat/benchfmt-compatible plain text. - Updated
benchcheck checkto acceptbase.json/head.json, parse each input once, and optionally write reconstructedbase.txt/head.txt. - Updated the benchmark GitHub Actions workflow to emit
base.json/head.jsonand runbenchcheckbeforebenchstatwhile preserving the check exit code.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/benchcheck/testjson.go | Introduces test2json parsing, benchmark text reconstruction, and structured failure extraction. |
| cmd/benchcheck/check.go | Switches check to parse .json inputs and adds flags to emit reconstructed plain-text outputs for benchstat. |
| cmd/benchcheck/main.go | Updates CLI docs to reflect .json inputs for the check subcommand. |
| cmd/benchcheck/main_test.go | Replaces text-scanner tests with parseTestJSON coverage for failures, crashes, and non-JSON tolerance. |
| .github/workflows/benchmark.yml | Updates workflow to write test2json outputs and use benchcheck to produce base.txt/head.txt before running benchstat. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace bufio.Scanner (fixed per-line token limit) with a bufio.Reader in parseTestJSON so a single oversized test2json event no longer aborts parsing and silently drops later failures/benchmark output; add a >1MiB regression test. Also merge stderr into base.json/head.json so the captured go test -json stream includes any stray tool diagnostics for troubleshooting (benchcheck already skips non-JSON lines).
dagood
left a comment
There was a problem hiding this comment.
Looks good, a couple alternatives for a structural nitpick.
| -o-status=status.json \ | ||
| base.txt head.txt | ||
| base.json head.json && rc=0 || rc=$? | ||
| go -C _go-infra tool benchstat "$GITHUB_WORKSPACE/base.txt" "$GITHUB_WORKSPACE/head.txt" | tee benchstat.txt || true |
There was a problem hiding this comment.
Would it make more sense to run benchstat as part of benchcheck, avoiding some shuffling of data through bash (and through files)?
There was a problem hiding this comment.
Took another look into this and the pinned x/perf only exposes the deprecated benchstat library whose output differs from the cmd/benchstat CLI we use. Reproducing the current format in-process would mean coupling to benchstat's internals and I feel like that would be a lot of work for a smaller result so maybe I would lean towards keeping it as a CLI call
There was a problem hiding this comment.
I'm not talking about it being in-process necessarily--the benchcheck tool could use os/exec. Maybe it's a wash, with the changes needed to do that. 🤷
(If nobody will be attempting this sequence of commands locally, we also don't have that reason to try to make benchcheck handle more for them.)
Replace the manual bash exit-code handling with two steps: benchcheck check runs last in its step so its exit code marks the job status normally, and benchstat runs as a separate if: always() step so the human-readable diff is still produced when a regression or failure fails the check step.
|
updated the reference here to test the changes |
| -o-status=status.json \ | ||
| base.txt head.txt | ||
| base.json head.json && rc=0 || rc=$? | ||
| go -C _go-infra tool benchstat "$GITHUB_WORKSPACE/base.txt" "$GITHUB_WORKSPACE/head.txt" | tee benchstat.txt || true |
There was a problem hiding this comment.
I'm not talking about it being in-process necessarily--the benchcheck tool could use os/exec. Maybe it's a wash, with the changes needed to do that. 🤷
(If nobody will be attempting this sequence of commands locally, we also don't have that reason to try to make benchcheck handle more for them.)
Replace the prefix-matching failure scanner with structured
test2jsonparsing, so failure detection no longer depends on matching
go testtext output line-by-line.
What changed
New
testjson.go—parseTestJSONreadsgo test -jsonoutput and:failevents (flushing the failing package/test's buffered output forcontext, dropping the
=== RUN/=== PAUSE/=== CONTframing lines),instead of pattern-matching
#,--- FAIL,FAIL\t,SIGSEGV:, andpanic:prefixes;outputevents, whichboth
benchfmt(regression parsing) andbenchstatconsume;check.go— now takesbase.json/head.json, parses each once forboth failures and benchmarks, and adds
-o-base-text/-o-head-textflagsthat write the reconstructed plain text for
benchstat. Removed the oldextractFailures,isCrashLine,appendFailuresFromFile, andparseBenchmarksFromFilehelpers.benchmark.yml— the bench steps now emitgo test -jsontobase.json/head.json. The "Compare and check" step runsbenchcheckfirst (capturing its exit code) so it can produce the reconstructed
base.txt/head.txt, then runsbenchstaton them, then propagates thecheck exit code — so
benchstat.txtis still produced even when aregression or failure makes
checkexit non-zero.Why
The text scanner was fragile: it relied on the exact shape of
go testoutput and on heuristics (e.g. cutting a crash trace at the first blank
line). Parsing the structured
test2jsonstream makes failure detectionrobust and lets us attribute output to the specific package or test that
failed.
Testing
gofmt,go vet,go test ./cmd/benchcheck/all clean; the text-scannerunit tests were replaced with
parseTestJSONtests covering testfailures, build failures, crashes, passing benchmark reconstruction, and
non-JSON tolerance.
test2jsoninputs: reconstructedbenchmark text is benchstat-ready,
failures.txtcaptures the failingtest and package summary, and
status.jsonreflects the failure.