perf: P0 bench-harness baseline (csvzen-bench module) - #18
Open
guizmaii wants to merge 13 commits into
Open
Conversation
- docs/performance-notes.md: snapshot of what governs CSV-writer throughput on the JVM, used as the seed for the optimization plan - docs/superpowers/specs/2026-05-03-csvzen-perf-design.md: design for the full csvzen 1.0 performance overhaul (six phases, P0-P5) - docs/superpowers/plans/2026-05-03-csvzen-perf-p0-bench-harness.md: detailed implementation plan for Phase 0 (bench harness baseline)
The four 10M-row Vector[A] data fixtures live concurrently for the trial. StringHeavy alone is ~3 GB; together they hit ~6 GB before GC headroom and JIT overhead. The plan's -Xms4g -Xmx4g OOM'd in setupTrial.
Run on MacBook Pro M3 Pro (36 GB), Corretto 25.0.2, off the current Writer-backed encoder. 24 main-bench cells + 18 buffer-sweep cells. Headline: - Mixed @ 100k null-sink: 33 ops/s, 34 MB/op alloc - DoubleHeavy @ 100k null-sink: 38 ops/s, 52 MB/op alloc (per-row Double.toString allocations dominate - direct P2 Schubfach target) - IntHeavy/StringHeavy @ 100k null-sink: 57/68 ops/s, ~106 KB/op - Buffer sweep is essentially flat (4 KB to 1 MB moves null-sink by < 5 %), consistent with the perf doc's observation that the BufferedWriter buffer isn't the bottleneck - the Writer/CharsetEncoder path is. The post-P1 buffer sweep on the byte[] path is what actually picks the new default. See SUMMARY.md for full numbers and per-phase target deltas.
There was a problem hiding this comment.
Pull request overview
Adds a new private bench SBT module so csvzen can capture JMH baselines for the current Writer-backed implementation before later performance phases change runtime internals. The PR also introduces supporting benchmark fixtures/tests, stores a P0 baseline result set, and exposes a temporary flushCount stub on FieldEmitter for future instrumentation.
Changes:
- Adds
csvzen-benchwith benchmark schemas, deterministic data generation, a null sink, the main 24-cell JMH suite, and a buffer-size sweep. - Extends the build with
sbt-jmh, aggregates the new module, and adds a temporaryFieldEmitter.flushCountsurface plus a unit test. - Commits benchmark documentation and captured P0 baseline outputs/design docs for the planned multi-phase performance work.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| project/plugins.sbt | Adds sbt-jmh plugin. |
| build.sbt | Adds and aggregates the new bench subproject. |
| .gitattributes | Routes benchmark JSON outputs through Git LFS. |
| modules/core/src/main/scala/com/guizmaii/csvzen/core/FieldEmitter.scala | Adds flushCount stub for future instrumentation. |
| modules/core/src/test/scala/com/guizmaii/csvzen/core/FieldEmitterSpec.scala | Adds a surface test for flushCount. |
| modules/bench/src/main/scala/com/guizmaii/csvzen/bench/Schemas.scala | Defines benchmark row schemas. |
| modules/bench/src/main/scala/com/guizmaii/csvzen/bench/BenchData.scala | Adds deterministic benchmark data generators. |
| modules/bench/src/main/scala/com/guizmaii/csvzen/bench/NullOutputStream.scala | Adds null sink for encoder-only measurements. |
| modules/bench/src/main/scala/com/guizmaii/csvzen/bench/CsvWriteBench.scala | Adds the main 24-cell JMH benchmark suite. |
| modules/bench/src/main/scala/com/guizmaii/csvzen/bench/BufferSweepBench.scala | Adds buffer-size sweep benchmarks. |
| modules/bench/src/test/scala/com/guizmaii/csvzen/bench/BenchDataSpec.scala | Tests benchmark data determinism/basic properties. |
| modules/bench/src/test/scala/com/guizmaii/csvzen/bench/NullOutputStreamSpec.scala | Tests null sink byte counting/reset behavior. |
| modules/bench/README.md | Documents benchmark layout and run commands. |
| modules/bench/results/p0-baseline/SUMMARY.md | Adds narrative summary of captured baseline results. |
| modules/bench/results/p0-baseline/csv-write.json | Adds raw main-benchmark result artifact (LFS). |
| modules/bench/results/p0-baseline/buffer-sweep.json | Adds raw sweep result artifact (LFS). |
| docs/superpowers/specs/2026-05-03-csvzen-perf-design.md | Adds multi-phase performance design spec. |
| docs/superpowers/plans/2026-05-03-csvzen-perf-p0-bench-harness.md | Adds detailed P0 implementation plan. |
| docs/performance-notes.md | Adds background notes guiding optimization priorities. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| */ | ||
| @State(Scope.Benchmark) | ||
| @BenchmarkMode(Array(Mode.Throughput)) | ||
| @OutputTimeUnit(TimeUnit.MILLISECONDS) |
| */ | ||
| @State(Scope.Benchmark) | ||
| @BenchmarkMode(Array(Mode.Throughput)) | ||
| @OutputTimeUnit(TimeUnit.MILLISECONDS) |
Comment on lines
+28
to
+35
| ```bash | ||
| sbt --client "bench/Jmh/run -prof gc -prof stack -rf json -rff modules/bench/results/p0-baseline/csv-write.json com.guizmaii.csvzen.bench.CsvWriteBench.*" | ||
| ``` | ||
|
|
||
| ### Buffer sweep (~10 minutes) | ||
|
|
||
| ```bash | ||
| sbt --client "bench/Jmh/run -prof gc -rf json -rff modules/bench/results/p0-baseline/buffer-sweep.json com.guizmaii.csvzen.bench.BufferSweepBench.*" |
Comment on lines
+1117
to
+1130
| ```bash | ||
| sbt --client "bench/Jmh/run -prof gc -prof stack -rf json -rff modules/bench/results/p0-baseline/csv-write.json com.guizmaii.csvzen.bench.CsvWriteBench.*" | ||
| ``` | ||
|
|
||
| Expected: completes in ~30 minutes (this is the full run — 24 benchmarks × 2 forks × (5 warmup + 10 measurement) iterations × 2s + the four large-size singleshot runs). On completion, sbt reports `[success]` and `csv-write.json` exists at the path passed to `-rff`. | ||
|
|
||
| If the run is interrupted, the JSON is *not* written. Restart from scratch — JMH does not resume. | ||
|
|
||
| - [ ] **Step 3: Run the buffer sweep and capture JSON** | ||
|
|
||
| Run: | ||
|
|
||
| ```bash | ||
| sbt --client "bench/Jmh/run -prof gc -rf json -rff modules/bench/results/p0-baseline/buffer-sweep.json com.guizmaii.csvzen.bench.BufferSweepBench.*" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-opens #17 — that PR closed unexpectedly when a
git lfs migrate importrewrote the entire reachable history (not just the unique branch commits). This branch is now properly rebased on the latestmain(4c2d1fe), and the LFS migration is scoped via--exclude-ref=mainso only the 13 unique commits got rewritten — main's commits keep their canonical SHAs.This PR lands Phase 0 of the csvzen performance overhaul (six-phase design at
docs/superpowers/specs/2026-05-03-csvzen-perf-design.md): a new privatecsvzen-benchsbt module with JMH benchmarks (4 schemas × 3 sizes × 2 sinks = 24 cells) plus a buffer-size sweep, against the current Writer-backed implementation. Baseline captured on a MacBook Pro M3 Pro / Corretto 25.0.2.The implementation plan is at
docs/superpowers/plans/2026-05-03-csvzen-perf-p0-bench-harness.md.What lands
New module —
modules/bench/:csvzen-bench: private (publish / skip := true), enablesJmhPlugin, depends oncore. Bench JVM is JDK 21 (rest of build stays on JDK 17 until P5).Schemas.scala: four 5-field flat case classes (Mixed,IntHeavy,DoubleHeavy,StringHeavy) derivingCsvRowEncoder. ASCII-onlyStringgeneration.BenchData.scala: deterministic seeded generators per schema (TDD).NullOutputStream.scala: pos-tracking, no-syscall sink (TDD).CsvWriteBench.scala: two JMH classes —CsvWriteBenchSmallMedium(Throughput, 1k + 100k rows) andCsvWriteBenchLarge(SingleShotTime, 10M rows). 24@Benchmarkmethods.BufferSweepBench.scala: 9-point@Paramsweep (4 KB → 1 MB) onMixed @ 100k, both sinks.README.md: run commands, profiler notes, results layout.results/p0-baseline/{csv-write.json, buffer-sweep.json, SUMMARY.md}: real baseline numbers from this hardware. JSONs are git-lfs tracked.Core module change:
FieldEmitter.scala: addsprivate[csvzen] def flushCount: Long = 0Lstub. P1 swaps in the real counter.Repo-level change:
.gitattributes: new rulemodules/bench/results/**/*.json filter=lfs diff=lfs merge=lfs -text. All future phase result JSONs land in LFS automatically.Headline numbers (full breakdown in
modules/bench/results/p0-baseline/SUMMARY.md)Hardware: MacBook Pro M3 Pro (36 GB), Corretto 25.0.2.
medium/null(= 5 ×Double.toStringper row × 100K rows). Onlarge/null(10M rows) that's 5.19 GB allocated to write a single CSV. P2 Schubfach has a clear target.Instant.toString+Double.toString).Writer/CharsetEncoderpath is the bottleneck, not the buffer. P1's post-rewrite sweep is what actually picks the new default.P1 / P2 / P3 targets (concrete, gated by these numbers)
IntHeavy/small/null(≥ 11.5K ops/s) andIntHeavy/medium/null(≥ 114 ops/s).gc.alloc.rate.normmust drop to ≈ 0 onIntHeavy/nullandStringHeavy/nullcells.DoubleHeavy/medium/nullallocation drops by ≥ 80 % (52.0 MB → ≤ 10.4 MB), throughput ≥ 1.5× (38 → ≥ 57 ops/s).StringHeavy-longschema must beat its post-P2 baseline by ≥ 1.5×.Plan-vs-implementation deviations
Files.deleteIfExistsdiscardsBoolean. Plan's verbatim teardown bodies failed Scala 3.3.7's-Wnonunit-statement. Fixed via the existing codebase convention (val _ = Files.deleteIfExists(...)).=style andprivate inlineordering get rewritten on compile.plugins.sbtre-padding. Adding the longerpl.project13.scalagroup triggered scalafmt to re-pad the existing 5 plugin lines.-Xms4g -Xmx4gforCsvWriteBenchLargeOOM'd insetupTrial(4 × 10M-row vectors hit ~6 GB live). Bumped to 10g.-rffpaths. Plan's-rff modules/bench/...doesn't survive sbt's JMH fork (sub-process resolves relative paths against a different cwd). Used absolute paths.-prof stackreturns NaN under JDK 25 + Vector API incubator — JMH's stack sampler doesn't sample cleanly. Not pursued; we rely on-prof gcfor the regression gate.Test plan
sbt --client test)sbt --client checkis green (scalafix + scalafmt)sbt --client "bench/Jmh/compile"succeeds — JMH annotation processor wires up cleanlyintHeavy_small_nullruns end-to-end and reports a numberBufferSweepBench.writeNullSinkwith twobufSizeparams runs end-to-endflushCount is reachable and starts at 0test passes against Writer-backed implementationcsv-write.json)buffer-sweep.json)SUMMARY.mdwritten with hardware info, full numbers, per-phase target deltasReproduce locally
Next steps after merge
≥ 2× IntHeavy/null-sinkand≥ 80 % DoubleHeavy alloc droptargets)..gitattributesalready routes their result JSONs to LFS automatically.