Skip to content

bench: restore JsonPipelineBenchmark + AvroPipelineBenchmark (closes #151)#154

Merged
eschizoid merged 1 commit into
mainfrom
fix/restore-pipeline-benchmarks
Jun 8, 2026
Merged

bench: restore JsonPipelineBenchmark + AvroPipelineBenchmark (closes #151)#154
eschizoid merged 1 commit into
mainfrom
fix/restore-pipeline-benchmarks

Conversation

@eschizoid

Copy link
Copy Markdown
Owner

Summary

Resolves #151. Both pipeline benchmarks have been compile-broken on `main` since the architecture-deepening pass removed the byte-level `MessagePipeline.apply(byte[])` shim — they still declared the field as `Function<byte[], byte[]>` and called `.apply(jsonBytes)` on what was now a `MessagePipeline<Map<String, Object>>` / `MessagePipeline`. `compileJmhJava` was failing across the whole `:benchmarks` module on `main`, which is why PR #153 had to exclude these two files to run any benchmark at all.

Changes

Rewrite the kpipe-side benchmark methods against the current pipeline API:

```java
final var value = pipeline.deserializeOrFail(bytes);
final var result = pipeline.process(value);
if (!(result instanceof Result.Passed passed)) {
throw new AssertionError("benchmark input should always pass: " + result);
}
bh.consume(pipeline.serialize(passed.value()));
```

Same per-record work as the old `pipeline.apply(bytes)` (deserialize → operators → serialize), just unrolled to match the new sealed-`Result` return shape. The pattern-match-or-fail keeps the bench loud if input ever falls into `Filtered` or `Failed` — silent assumption breakage would falsify the numbers.

Manual SerDe paths are untouched; they never used `.apply(byte[])`.

Smoke numbers (1 iter × 1 fork × 1 warmup, sanity-only)

Benchmark ops/s
`JsonPipelineBenchmark.kpipeJsonPipeline` 663,820
`JsonPipelineBenchmark.manualJsonSerDeChained` 258,721
`JsonPipelineBenchmark.manualJsonSingleSerDe` 836,126
`AvroPipelineBenchmark.kpipeAvroPipeline` 867,447
`AvroPipelineBenchmark.kpipeAvroMagicPipeline` 657,508
`AvroPipelineBenchmark.manualAvroMagicHandling` 1,042,793

The single-SerDe-tax story these benches exist to surface is intact (kpipe `663k` vs manual chained `259k` on JSON, ~2.6×).

Coordination with PR #153

PR #153 added a temporary exclusion of both files in `benchmarks/build.gradle.kts` to unblock the JMH source set. If this PR merges after #153, the exclusion lines should also be removed (they'll point at compilable files at that point). If this PR merges before #153, the exclusion still works but is no longer necessary — #153's rebase can drop it.

Test plan

  • `./gradlew :benchmarks:compileJmhJava` clean
  • `./gradlew :benchmarks:jmh -Pjmh.includes='(JsonPipelineBenchmark|AvroPipelineBenchmark)' -Pjmh.iterations=1 -Pjmh.warmupIterations=1 -Pjmh.fork=1` produces all 6 cells
  • `./gradlew spotlessCheck` clean
  • CI green

…151)

Both benchmarks were broken on main since the architecture-deepening
pass removed the byte-level MessagePipeline.apply(byte[]) shim — they
still declared the field as Function<byte[], byte[]> and called
.apply(jsonBytes) on what was now a MessagePipeline<Map<String,Object>>
(or <GenericRecord>). The classes didn't compile, which broke
compileJmhJava across the whole :benchmarks module.

Rewrite the kpipe-side benchmark methods against the current API:

  final var value = pipeline.deserializeOrFail(bytes);
  final var result = pipeline.process(value);
  if (!(result instanceof Result.Passed<T> passed)) {
    throw new AssertionError("benchmark input should always pass: " + result);
  }
  bh.consume(pipeline.serialize(passed.value()));

Same per-record work as before (deserialize → operators → serialize),
just unrolled to match the new return shape. The pattern-match-or-fail
shape keeps the bench loud if the input ever falls into Filtered or
Failed — silent assumption breakage would falsify the numbers.

Smoke run (1 iter × 1 fork × 1 warmup) numbers, sanity-only:
- JSON kpipe pipeline:           663k ops/s
- JSON manual SerDe chained:     259k ops/s   (the SerDe tax this bench exists to show)
- JSON manual single SerDe:      836k ops/s
- Avro kpipe pipeline:           867k ops/s
- Avro kpipe magic (zero-copy):  658k ops/s
- Avro manual magic (copyRange): 1.04M ops/s

The single-SerDe-tax story is intact: kpipe ~2.6× faster than the
chained manual path at the JSON cell, which is what these benches
were designed to surface.

Manual side untouched — those paths never used .apply(byte[]).
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.52%. Comparing base (802197d) to head (fab62f9).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #154      +/-   ##
============================================
+ Coverage     76.48%   76.52%   +0.03%     
- Complexity      760      761       +1     
============================================
  Files            75       75              
  Lines          2871     2871              
  Branches        357      357              
============================================
+ Hits           2196     2197       +1     
  Misses          503      503              
+ Partials        172      171       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@eschizoid eschizoid merged commit c221b4f into main Jun 8, 2026
4 checks passed
eschizoid added a commit that referenced this pull request Jun 8, 2026
Two follow-ups from the IDE-inspection sweep, this time covering
benchmarks/ and examples/ (the first pass only hit lib/).

**Benchmark teardown dedup.** Two invocation-context inner classes in
ParallelProcessingBenchmarkInfrastructure (Raw + Share) had the same
17-line shutdown block: lower the `running` flag, join the poll loop,
shutdown the executor, await termination (return ignored). IntelliJ
flagged it as duplicated-code; the ignored-return was a separate
finding sitting in the same blocks. Extract to
`stopPollLoopAndExecutor(AtomicBoolean, Thread, ExecutorService)` —
single source of truth, and the `awaitTermination` return is now
logged at WARNING (don't silently swallow) so a slow executor doesn't
disappear from the report.

**Plain-text URL in Javadoc.** Two example files had bare
`http://...` URLs inside triple-slash markdown doc comments, which
IntelliJ flags as plain-text-url-in-javadoc. Wrap in angle brackets
so javadoc renders them as autolinks (Markdown URL convention).

**Drop now-redundant Json/Avro exclusion.** PR #153 added a
benchmarks/build.gradle.kts source-set exclusion to unblock JMH
compilation when those benchmarks were broken. PR #154 restored the
benchmarks but #153 merged after, leaving the exclusion in place
against now-compilable files. Remove the dead exclusion block.
eschizoid added a commit that referenced this pull request Jun 8, 2026
* chore: address IntelliJ inspection warnings (safe sweep)

Mechanical clean-up driven by a multi-agent IDE-inspection survey
(workflow over 12 lib modules, 375 findings total). Restricts itself
to the categories that are safe to fix without judgment.

Three categories addressed:

1. unused-pattern-variable (15 sites): rename `__` to `_` in
   sealed-Result pattern matches. Java 22+ unnamed pattern;
   mechanical with no behavior change.
2. redundant-cast (13 → 12 sites): remove casts the compiler can
   infer from context. The (long) cast on
   DispatcherIntegrationTest.java:105 stays — it's the boxing path
   from int to Map<TopicPartition, Long>, where IntelliJ's
   "redundant" verdict was actually wrong (compile-fail without it).
3. redundant-suppression (14 sites): delete stale @SuppressWarnings
   annotations that no longer match any real warning.

Plus two unresolved-javadoc-symbol fixes (ERROR severity):
- KPipeDlqTest: qualify [Builder#withDeadLetterQueue(...)] →
  [KPipeConsumer.Builder#withDeadLetterQueue(...)]
- ParallelDispatcherTest: qualify [#close()] →
  [ParallelDispatcher#close()]

What's NOT in this PR (deferred to per-site review):

- unused-parameter (51): almost all are deliberate-ignore in test
  lambdas; needs `_` rename per-site, not a blanket fix.
- try-with-resources (39): rewriting integration tests would change
  Handle / KPipeConsumer shutdown semantics.
- busy-wait (17): intentional poll-until-condition loops in shutdown
  drain and KEY_ORDERED retry paths.
- nullable-handling (16): per-site judgment, especially AvroFormat
  registry-mode hot path.
- unused-method (11): public fluent-facade methods kept as escape
  hatches; documented in docs/escape-hatches.md.
- unchecked-assignment (9): Mockito raw-type captures and Kafka
  ConsumerRecord/ProducerRecord generics need per-site decisions.

Test plan:
- ./gradlew test  (green locally)
- ./gradlew spotlessCheck  (green)
- ./gradlew compileJava compileTestJava  (green)

* refactor: dedup common consumer-config block in DefaultSink + DefaultBatchSink

Both sinks ended their start() with the same 6-line "apply optional
consumer config to the builder" block (retry, backpressure, metrics,
errorHandler, deadLetterTopic, pollTimeout). IntelliJ flagged it as a
9-line code-duplication.

Extract to a package-private DefaultStream.applyCommonConsumerConfig
helper. DefaultSink calls it and then adds its tracer +
circuit-breaker (those two are intentionally not on DefaultBatchSink
today — adding them would be a behavior change, deferred).

* refactor: dedup JMH teardown boilerplate + fix javadoc URL lints

Two follow-ups from the IDE-inspection sweep, this time covering
benchmarks/ and examples/ (the first pass only hit lib/).

**Benchmark teardown dedup.** Two invocation-context inner classes in
ParallelProcessingBenchmarkInfrastructure (Raw + Share) had the same
17-line shutdown block: lower the `running` flag, join the poll loop,
shutdown the executor, await termination (return ignored). IntelliJ
flagged it as duplicated-code; the ignored-return was a separate
finding sitting in the same blocks. Extract to
`stopPollLoopAndExecutor(AtomicBoolean, Thread, ExecutorService)` —
single source of truth, and the `awaitTermination` return is now
logged at WARNING (don't silently swallow) so a slow executor doesn't
disappear from the report.

**Plain-text URL in Javadoc.** Two example files had bare
`http://...` URLs inside triple-slash markdown doc comments, which
IntelliJ flags as plain-text-url-in-javadoc. Wrap in angle brackets
so javadoc renders them as autolinks (Markdown URL convention).

**Drop now-redundant Json/Avro exclusion.** PR #153 added a
benchmarks/build.gradle.kts source-set exclusion to unblock JMH
compilation when those benchmarks were broken. PR #154 restored the
benchmarks but #153 merged after, leaving the exclusion in place
against now-compilable files. Remove the dead exclusion block.

* test: pin Handle defaults + DefaultHandle.startAndWrap exception path

Targets the two real coverage gaps in lib/kpipe-api/src/main that
codecov flagged:

1. HandleDefaultsTest — Handle.java was at 0% / 4 tracked lines.
   The defaults aren't decoration: topKeyQueueDepths(n) has a real
   guard (n <= 0 → IllegalArgumentException) and a real return shape
   (immutable empty list), and close() commits to a 5-second
   shutdownGracefully timeout that every try-with-resources caller
   relies on. The test class uses a minimal Handle subclass that
   overrides only the abstract methods so the defaults actually run,
   then pins each: bad n, valid n, default close timeout.

2. DefaultHandleStartAndWrapTest — DefaultHandle.startAndWrap is the
   only non-delegator in the class (everything else just forwards to
   the consumer, which is not worth a mock-against-mock test). The
   factory has a non-trivial error path: if consumer.start() throws,
   it must close the consumer (so AutoCloseable holds when the caller
   never gets a Handle back), and if close() also throws, the
   secondary exception must be addSuppressed onto the original. The
   test pins happy path, start-failure, start+close-failure
   (suppressed exception attached), and null-consumer rejection.

Skipped on purpose:
- DefaultHandle delegators (isHealthy / metrics / awaitShutdown /
  shutdownGracefully) — testing them with mocks verifies the mock,
  not behavior. Coverage % would rise, real safety wouldn't.
@eschizoid eschizoid deleted the fix/restore-pipeline-benchmarks branch June 8, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bench: restore JsonPipelineBenchmark + AvroPipelineBenchmark (stale since architecture-deepening pass)

1 participant