{"id":"ddc76636-a30a-481f-8edf-9ed9bb894e81","slug":"benchcat","kind":"skill","name":"benchcat","summary":"Super cute 🐱 Benchmark graph generation","owner":"unk-404","repoUrl":"https://github.com/unk-404/benchcat","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-10-19T09:57:58.000Z","addedAt":"2026-10-22T00:04:58.994Z","logoUrl":"https://avatars.githubusercontent.com/u/311131103?u=e5c803355128143c00be6351c8dda553c1e4944f&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["writing"],"tags":["benchmark","tool"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T10:06:15.874Z","sample":false,"url":"https://conduits.dev/tools/benchcat","language":"Rust","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Benchcat\n\nRender benchmark results as a self-contained SVG card for a README.\n\nA personal tool, published as-is. It reads what a benchmark runner already writes, draws one SVG, and\nstops there — committing it, uploading it, or commenting it on a pull request is your workflow's job.\n\n![benchmark card](assets/examples/01-basic.svg)\n\n## Install\n\n```bash\ncargo install --path crates/benchcat\n```\n\nOr download a prebuilt binary from Releases (`benchcat-<target>.tar.gz`, checksums in `SHA256SUMS`).\n\nA TrueType/OpenType face is required; `fonts/` ships an OFL catalogue (see [Fonts](#fonts)):\n\n```bash\nbenchcat render bench.jsonl -o assets/bench.svg --font quicksand\nbenchcat render bench.jsonl -o assets/bench.svg --font-file MyFont.ttf\n```\n\n## Quick start\n\n```bash\nbenchcat render bench.jsonl       -o assets/bench.svg                 # benchmark_test JSONL\nbenchcat render report.json       -o assets/bench.svg                 # canonical JSON\nbenchcat render results.csv       -o assets/bench.svg                 # CSV\nbenchcat render target/criterion  -o assets/bench.svg --format criterion\nbenchcat render bench.json        -o assets/bench.svg --format hyperfine\nbenchcat render go-bench.txt      -o assets/bench.svg                 # Go testing\nbenchcat render jmh.json          -o assets/bench.svg                 # JMH / kotlinx-benchmark\nbenchcat render google.json       -o assets/bench.svg                 # Google Benchmark JSON\nbenchcat render swift.json        -o assets/bench.svg                 # swift-benchmark JSON\nbenchcat render bt-human.txt      -o assets/bench.svg                 # benchmark_test human\nbenchcat render runner.txt        -o assets/bench.svg                 # benchmark_runner\nbenchcat render benchmarkdotnet.md -o assets/bench.svg                # BenchmarkDotNet\n```\n\nSame inputs produce the same bytes, so a committed SVG only changes when the data does:\n\n```bash\ngit diff --quiet -- assets/bench.svg || git commit -am \"bench: refresh [skip ci]\"\n```\n\n## Input\n\n`--format` accepts `auto` (default), `json`, `jsonl`, `csv`, `benchmark_test`, `criterion`,\n`hyperfine`, and the preset formats `go`, `phpbench`, `jmh` (alias `kotlinx`), `google`,\n`google-console`, `swift`, `techempower`, `androidx`, `benchmark-test-human`,\n`benchmark-test-benchmarkjs`, `benchmark-runner` and `benchmarkdotnet` (aliases `csharp`, `dotnet`). Directories are walked recursively;\ncriterion's `report/` folder is skipped.\n\nSource presets live in [`sources/*.oops`](sources/README.md) and are compiled into the binary at\nbuild time by `benchcat-source-gen`; adding a format does not require touching the Rust input code.\n\nCanonical shape, `benchcat/report@1`:\n\n```jsonc\n{\n  \"schema\": \"benchcat/report@1\",\n  \"meta\": {\n    \"tool\": \"benchmark_test\",\n    \"tool_version\": \"0.9.1\",\n    \"commit\": \"cd5cd87\",\n    \"platform\": { \"os\": \"Windows\", \"arch\": \"x86_64\", \"cpu\": \"Ryzen 9 7950X\", \"cores\": 32 }\n  },\n  \"benchmarks\": [{\n    \"id\": \"file.read.8MiB\",          // stable key: matching, ordering, comparisons\n    \"group\": \"File I/O\",\n    \"label\": \"Sequential read\",      // display only\n    \"note\": \"8 MiB blocks · 1 GiB file\",\n    \"unit\": \"B/s\",                   // a dimension, not a decoration\n    \"value\": 2254857830,\n    \"direction\": \"higher\",           // \"lower\" for latency\n    \"params\": { \"block\": \"8MiB\" },   // structured, available to row templates\n    \"stats\": { \"rme_pct\": 1.21, \"samples\": 5 },\n    \"deterministic\": false\n  }]\n}\n```\n\nUnits carry a dimension (`B/s`, `IOPS`, `ns/op`, `instr`, `×`), so values are formatted from the data.\nBars are scaled inside one dimension, and `direction: \"lower\"` inverts them so a longer bar always\nmeans better.\n\n### criterion\n\n```bash\nbenchcat render target/criterion -o assets/bench.svg --format criterion\n```\n\nWalks `<bench>/new/estimates.json`, reads the sibling `benchmark.json` for `full_id` and `throughput`,\nand turns `Bytes`/`Elements` throughput into `B/s`/`ops/s` (`ns/op`, lower-is-better otherwise). The\n95 % confidence interval becomes the ±RME column.\n\n### hyperfine\n\n```bash\nbenchcat render bench.json -o assets/bench.svg --format hyperfine\n```\n\nReads `--export-json` v2 (and the legacy v1 shape), using the wall-clock mean with lower-is-better;\nseconds are formatted as ms/µs.\n\n### Go, JMH, Google Benchmark, Swift, PHPBench, TechEmpower, AndroidX, Dart\n\n| Producer | Typical command / file | `--format` |\n|---|---|---|\n| Go | `go test -bench . -benchmem > bench.txt` | `go` |\n| JMH | `-rf json -rff jmh.json` | `jmh` |\n| kotlinx-benchmark | default JSON report | `jmh` (`kotlinx`) |\n| Google Benchmark | `--benchmark_format=json` | `google` |\n| Google Benchmark console | copy the `BM_...` table | `google-console` |\n| swift-benchmark | `--format json` | `swift` |\n| PHPBench | `phpbench run --output=json` | `phpbench` |\n| TechEmpower | `results.json` from the framework runner | `techempower` |\n| AndroidX Benchmark | `benchmarkData.json` | `androidx` |\n| `benchmark_test` human | default CLI output | `benchmark-test-human` |\n| `benchmark_test` BenchmarkJS | `--output=benchmarkjs` | `benchmark-test-benchmarkjs` |\n| `benchmark_test` JSONL | `--output=jsonl` | `benchmark_test` |\n| `benchmark_runner` | exported stdout / report | `benchmark-runner` |\n| BenchmarkDotNet | Markdown or CSV table | `benchmarkdotnet` (`csharp`, `dotnet`) |\n\nAll of these are auto-detected from the file content. See [`sources/README.md`](sources/README.md) for\nthe preset syntax.\n\n## CLI\n\n| Flag | Meaning |\n|---|---|\n| `INPUT...` | Files or directories |\n| `--out <FILE>` | Output path; `-` writes to stdout |\n| `--format <FORMAT>` | `auto`, `json`, `jsonl`, `csv`, `benchmark_test`, `criterion`, `hyperfine`, `go`, `phpbench`, `jmh`, `google`, `google-console`, `swift`, `techempower`, `androidx`, `benchmark-test-human`, `benchmark-test-benchmarkjs`, `benchmark-runner`, `benchmarkdotnet` |\n| `--config <FILE>` | Defaults to `./benchcat.toml` when present |\n| `--font <NAME>` | Family inside `./fonts` (or `BENCHCAT_FONT_DIR`), e.g. `quicksand` |\n| `--font-file <FILE>` | Explicit TTF/OTF path |\n| `--title` / `--subtitle` / `--footer` | Override the three text lines |\n| `--width <PX>` | Canvas width (default 920) |\n| `--min-height <PX>` | Keep the card at least this tall |\n| `--max-rows <N>` | Render only the first N rows |\n| `--theme <NAME>` | `pastel` or `dark` |\n| `--stdout` | Also print the SVG |\n\n`benchcat formats` lists the accepted format names.\n\nLogging is off unless you opt in with `--log info` (or `--log debug`); it writes stage timings to stderr.\nThe SVG on stdout (`-o -`) is never mixed with logs, and CI does not treat stderr output as failure.\n\n## Config\n\n`benchcat.toml` is discovered from the working directory; `--config` overrides it.\n\n```toml\n[font]\nfamily = [\"mplus-rounded\"]  # one stem, \"a,b\", or [\"a\",\"b\"]\nenable_guaranteed_font = false   # opt-in per-character fallback; see below\nenable_system_font = false        # opt-in family lookup in installed system fonts\n\n[render]\ntitle = \"blit benchmarks\"\nsubtitle = \"{platform|local} · {tool} {tool_version} · commit {commit}\"\nfooter = \"Generated from {rows} rows\"\nwidth = 920\nmin_height = 0\ntheme = \"pastel\"          # or \"dark\"\nkaomoji = \"(◕‿◕)♡\"\nmax_rows = 12\nbar_scale = \"row\"          # row | source | global (multi-source only)\nbar_curve = \"linear\"       # linear | log (multi-source only)\nmissing = \"skip\"           # skip | placeholder (multi-source only)\n\n[[series]]\nlabel = \"Linux x64\"\ninput = \"bench-linux.jsonl\"\ncolor = \"#7FD1B9\"\n\n[[series]]\nlabel = \"Windows x64\"\ninput = \"bench-windows.jsonl\"\ncolor = \"#8CB8F0\"\n```\n\n`family` accepts a single stem (`\"mplus-rounded\"`), a comma-separated string (`\"mplus-rounded,\nzcool-kuaile\"`) or a list (`[\"mplus-rounded\", \"zcool-kuaile\"]`); entries are tried in order, so old\nsingle-string configs are unchanged.\n\n`enable_guaranteed_font = false` keeps the original single-face behaviour, so existing configs and\ncards are unchanged. When it is `true`, the configured face is tried first and characters it lacks come\nfrom a fixed list: our `fonts/` catalogue (`mplus-rounded`, `zen-maru-gothic`, `zcool-kuaile`,\n`quicksand`) and then a short system list, at most four faces in total. The list is not configurable. If\nthe configured face is missing or unreadable, the first guaranteed face becomes the primary. Runs fully\ncovered by the primary face keep byte-identical output; only runs that actually use a fallback switch to\nper-glyph scaling.\n\n`enable_system_font = false` (default) keeps family lookup inside the repository. When it is `true`, a\nfamily can also match an installed system face, matched by name (case and punctuation insensitive) and\nincluding TrueType/OpenType collections. Repository fonts still win, and the option is off by default so\ncards stay reproducible across machines.\n\nFor CI and other automated builds, keep both `enable_guaranteed_font` and `enable_system_font` off: they\ncan depend on fonts installed on the runner. Use the repository catalogue (`font`) or `font_file` instead.\n\nTwo or more `[[series]]` switch to the comparison layout: one row per operation, one bar per source, a\nlegend, and one value per bar. Bars are linear and scaled to the row's own best value by default\n(`bar_scale = \"row\"`); `bar_curve = \"log\"` compresses wide ranges, `bar_scale = \"source\"` compares each\nsource against its own best across rows, and `bar_scale = \"global\"` uses one reference per unit family\nfor the whole card. Single-source cards keep the log scale against the best row of the unit family.\n\nWhen a row is missing a source, `missing = \"skip\"` (default) draws no bar and the row is shorter;\n`missing = \"placeholder\"` keeps the slot and prints `—`.\n\n### Row mapping\n\n`[[row]]` renames rows, overrides units and transforms values. Rules apply in order, and matching rows\ntake the rule's position in the card.\n\n```toml\n[[row]]\nid = \"file.read.8MiB\"\nlabel = \"Sequential read\"\nnote = \"8 MiB blocks · 1 GiB file\"\n\n[[row]]\nmatch = '^pool\\.read\\.random4K\\.w(\\d+)$'\nlabel = \"Pool read · $1 workers\"\nnote = \"4 KiB · batch 32\"\nvalue = \"value * $1 * 32\"\nunit = \"IOPS\"\ngroup = \"Thread pool\"\n```\n\n- `id` is an exact match; `match` is a regex whose groups become `$1`… in templates and values.\n- `label`/`note` support `$1` captures and `{param}` placeholders; an empty `note` clears it.\n- `value` is a small expression: numbers, `value`, `$1`…, `+ - * /`, parentheses and\n  `min`/`max`/`abs`/`log10`/`sqrt`/`round`.\n- `group` (or a `group` field on the input) puts the row under a section heading; rows sharing a group\n  are drawn together, in the order the group first appears.\n- `only_matched = true` under `[render]` drops unmatched rows; the default appends them.\n\n### Placeholders\n\n`{commit}`, `{commit:full}`, `{branch}`, `{tool}`, `{tool_version}`, `{tool:full}`, `{platform}`,\n`{os}`, `{os_version}`, `{arch}`, `{cpu}`, `{cores}`, `{ram_gib}`, `{runner}`, `{rows}`, `{font}`.\n\nValues come from the input document plus `GITHUB_SHA`, `RUNNER_OS` and `RUNNER_ARCH` when present.\n`{key|fallback}` renders the fallback when the value is empty; `{{` is a literal `{`.\n\n## Fonts\n\n`fonts/` holds an OFL-only catalogue; pick one with `[font] family` or `--font`. The full table —\ncoverage, file size and a specimen for every family — is on the\n[Fonts wiki page](https://github.com/unk-404/benchcat/wiki/Fonts).\n\nEach family ships its `-OFL.txt` next to the binary. The generated specimens are committed under\n`assets/fonts/<file-stem>.svg`; CI mirrors them to the wiki (one heading per family, images linked from\nthis repository). Specimens draw every glyph the face cannot provide as a small pastel cat.\n\n## Examples\n\n| Figure | Shows |\n|---|---|\n| [`01-basic.svg`](assets/examples/01-basic.svg) | single source, mixed units, RME column |\n| [`02-comparison.svg`](assets/examples/02-comparison.svg) | same operations, three platforms, legend |\n| [`03-units.svg`](assets/examples/03-units.svg) | `B/s`, `IOPS`, `ns/op`, `instr` from their units |\n| [`04-min-height.svg`](assets/examples/04-min-height.svg) | `min_height` |\n| [`05-dark.svg`](assets/examples/05-dark.svg) | `theme = \"dark\"` |\n| [`06-max-rows.svg`](assets/examples/06-max-rows.svg) | `max_rows` |\n| [`07-dynamic-height.svg`](assets/examples/07-dynamic-height.svg) | height follows the rows |\n| [`08-cjk.svg`](assets/examples/08-cjk.svg) | Japanese labels |\n| [`09-notes.svg`](assets/examples/09-notes.svg) | notes and structured params |\n| [`10-criterion.svg`](assets/examples/10-criterion.svg) | `--format criterion` |\n| [`11-hyperfine.svg`](assets/examples/11-hyperfine.svg) | `--format hyperfine` |\n| [`12-rows.svg`](assets/examples/12-rows.svg) | `[[row]]` mapping |\n| [`13-sections.svg`](assets/examples/13-sections.svg) | `group` fields become section headings |\n| [`14-missing.svg`](assets/examples/14-missing.svg) | a missing source is skipped and the row shrinks |\n| [`15-bar-scale.svg`](assets/examples/15-bar-scale.svg) | `bar_scale = \"global\"` |\n| [`16-font-fallback.svg`](assets/examples/16-font-fallback.svg) | English only → Japanese fallback → Chinese/Korean → Russian (Noto Sans) |\n\n```bash\ncargo run --example gallery                      # SVGs into assets/examples/\ncargo run --example gallery -- --only 14 --png    # one example, plus a PNG preview\ncargo run --example gallery -- --list             # example names\ncargo run --example specimen -- --font quicksand   # one specimen\ncargo run --example specimen -- --list            # font stems\n```\n\n\n## Source cards\n\nEvery supported source gets a mock card under [`assets/sources/`](assets/sources/). Each card labels its\nOOPS version (`v1`, `v2`) or `built-in`. The catalogue is [`catalogue.json`](assets/sources/catalogue.json);\nthe same images populate the\n[Sources wiki page](https://github.com/unk-404/benchcat/wiki/Sources).\n\n```bash\ncargo run --example sources                     # all source cards into assets/sources/\ncargo run --example sources -- --only go,jmh    # one or more cards\ncargo run --example sources -- --list           # source names\n```\n\nInputs are generated under `target/source-examples/`; the committed SVGs and catalogue are deterministic.\nCI regenerates them, checks the diff and refreshes the wiki page after asset pushes.\n\n## GitHub Action\n\nThe action installs the right binary (cached, checksum-verified) and runs one render. It does not touch\ngit, tokens or pull requests.\n\n```yaml\n- uses: unk-404/benchcat@master\n  id: bench\n  with:\n    input: target/criterion\n    format: criterion\n    out: assets/benchmarks.svg\n    config: benchcat.toml\n```\n\n| Input | Default | Meaning |\n|---|---|---|\n| `input` | — | Files or directories, space or newline separated |\n| `format` | `auto` | Same values as `--format` |\n| `config` | — | `benchcat.toml` (auto-discovered when omitted) |\n| `out` | `assets/benchmarks.svg` | Output path |\n| `font` | — | One or more catalogue families, comma separated, e.g. `quicksand,zcool-kuaile` |\n| `font_file` | — | Custom TTF/OTF; the bundled catalogue needs no file |\n| `version` | `auto` | Release tag; `auto` follows the action ref, else `latest` |\n| `tool_path` | — | Use an existing binary, no download |\n\nThe action carries the OFL font catalogue and points the renderer at it at runtime: do not add,\ndownload or commit fonts. Leave `font` empty for `mplus-rounded`, or name a catalogue family. Use\n`font_file` only for a custom face you already have.\n\nOutputs: `svg` and `bytes`. The step summary records both. The action derives its own repository from `GITHUB_ACTION_PATH` (GitHub leaves\n`GITHUB_ACTION_REPOSITORY` empty inside composite steps) and falls back to `GITHUB_REPOSITORY` for a local\n`uses: ./`, never the caller's repository. The checksum is verified against the release. Set `BENCHCAT_RELEASE_BASE` to a mirror when GitHub release downloads are slow.\n\nReady-made workflows are in [`templates/`](templates/):\n\n- [`rust-criterion.yml`](templates/rust-criterion.yml)\n- [`dart-benchmark-test.yml`](templates/dart-benchmark-test.yml)\n- [`hyperfine-matrix.yml`](templates/hyperfine-matrix.yml)\n\n## For agents\n\n- [`skills/benchcat/SKILL.md`](skills/benchcat/SKILL.md) — integration: one action step,\n  inputs, formats, config, verification, failure modes.\n- [`skills/benchcat-dev/SKILL.md`](skills/benchcat-dev/SKILL.md) — development: module map,\n  layout engine, determinism rules, fonts, gallery and release workflow.\n\n## Releases\n\nTags are `vYYYY.MM.DD-<short-sha>` (no semantic versions). Each release carries the six target\narchives, `SHA256SUMS`, `benchcat-assets.tar.gz` (gallery, specimens and source cards), and SLSA attestations.\nThe complete TTF catalogue is also published as `benchcat-fonts.7z` on the rolling\n[`fonts-latest` prerelease](https://github.com/unk-404/benchcat/releases/tag/fonts-latest); it is\ndeliberately not marked latest, so the action still resolves a binary release.\n\n## License\n\nMIT OR Apache-2.0. The bundled fonts are under the SIL Open Font License 1.1; each\n`fonts/<name>-OFL.txt` carries the original copyright notice.\n"}