Storage Benchmark Kit
Universal entry point for all AI agents. This file is the standard entry point for AI coding agents (Devin, Claude Code, Cursor, GitHub Copilot, Continue, Aider, OpenAI Codex, Windsurf, etc.) working in this repository. It tells the agent what SBK is, how to build and verify it, what conventions to follow, where things live, and what the common gotchas are.
Agent-specific configurations:
- Devin: See
.devin/skills/for executable skills- Cursor: See
.cursor/rules/sbk.mdc;.cursorrulesis the legacy pointer- Aider: See
.aider.conf.ymlfor Aider configuration- Codex and Windsurf: consume this root
AGENTS.mddirectly- Benchmark execution: any agent may use the portable
.devin/skills/sbk-benchmark-runner/and.devin/skills/sbk-distributed-benchmark-runner/knowledge packsHumans: see README.md for the end-user manual and docs/sbk-internals.md for the internal design.
SBK (Storage Benchmark Kit) is a Java framework for benchmarking any S3-compatible / message-queue / file-system / database storage. It runs identical measurement code against any backend via a small pluggable driver SPI.
./gradlew). JDK 25 required.master.| Module | Role | When you edit it |
|---|---|---|
perl/ |
PerL — Performance Logger library (lock-free queues, latency windows, percentile math). Heart of the framework. | Rarely. Only when changing core measurement behaviour. |
sbk-web-console/ |
Independent Local Web Console runtime (HTTP server/client, protocol DTOs, UI resources). | When changing shared WebLogger transport or browser behavior. |
sbk-api/ |
The benchmark harness. Defines Storage<T> SPI, RWLogger SPI, SbkBenchmark, Sbk main bootstrap. |
When changing the harness, CLI flags, the SPI, or how loggers work. |
sbm/ |
SBM — Storage Benchmark Monitor (gRPC aggregator on port 9717). | When changing distributed aggregation. |
sbk-yal/ |
YML-driven launcher (single-node). | Rarely. |
sbk-gem/ |
SBK-GEM — SSH-based distributed launcher. | When changing the multi-host orchestration. |
sbk-gem-yal/ |
YML-driven SBK-GEM. | Rarely. |
drivers/<name>/ |
One subdirectory per storage backend. 52 are enabled in the aggregate build; disabled drivers and a template also remain in tree. | When adding or fixing a driver. This is the most common change. |
For new drivers, see docs/DRIVER_SPECIFICATION.md (spec template + worked example) and docs/AGENT_RECIPES.md (“Add a storage driver” recipe).
# Default build — runs compile + checkstyle + tests on every module.
./gradlew check
# Build the launchable scripts at ./build/install/sbk/bin/sbk
./gradlew installDist
# Build a single driver (much faster while iterating)
./gradlew :drivers:minio:check
./gradlew :drivers:minio:compileJava
# Smoke-test against the public MinIO sandbox (no credentials needed)
./build/install/sbk/bin/sbk -class minio -writers 1 -size 100 -seconds 30
# Local file-system write benchmark (1 MiB records, 8 writers, 60 s)
./build/install/sbk/bin/sbk -class file -file /tmp/sbk.bin \
-writers 8 -size 1048576 -seconds 60
# Help / list available drivers
./build/install/sbk/bin/sbk -help
Verification is proportional to the affected surface:
./gradlew :drivers:<name>:check, :sbk-api:check, or :perl:check../gradlew check for source, dependency, or build-logic changes../gradlew installDist when runtime packaging, discovery, launchers,
drivers, or loggers are affected.play.min.io for S3). Compile-clean is not sufficient.git diff --check; do not claim unrelated backend testing.Report exact commands and distinguish a pass from a check that was not run or could not run.
For a release candidate, use the authoritative gate rather than assembling a manual subset:
./gradlew clean releasecheck \
-Pprofile=release \
-PreleaseInventory=/secure/sbk-release-inventory.properties \
--no-daemon --rerun-tasks
See docs/RELEASE_QUALIFICATION.md. A release
profile must fail when mandatory remote infrastructure is unavailable; it must
not convert missing GEM hosts or required backend coverage into a successful
skip. Use -Pprofile=local-docker for automatic disposable two-node
GEM functional coverage; it does not replace real-host release evidence.
Release qualification and publication are independent. Only an authorized
maintainer may dispatch an actual release with the root publish task and the
exact -PreleaseConfirm=RELEASE-<version> confirmation. The publication
workflow does not invoke or require releasecheck. The task uses locally
supplied DOCKER_USERNAME and DOCKER_PASSWORD credentials for Docker Hub,
then sends only the public image digest to GitHub. It returns after dispatch,
so the maintainer must monitor the asynchronous workflow and verify the public
tag, assets, packages, and container manifests. See
docs/RELEASE_PUBLICATION.md for credentials,
prerelease/resume controls, the non-publishing artifact/container dry run, and
the complete publication contract.
| Path | Convention |
|---|---|
drivers/<name>/build.gradle |
Subproject build script. Declares the driver’s vendor-SDK dependency. |
drivers/<name>/src/main/java/io/sbk/driver/<Name>/<Name>.java |
The Storage<T> impl. Class name = PascalCase of the driver dir name. Match the package case (e.g. MinIO driver dir → io.sbk.driver.MinIO package → MinIO.java). |
drivers/<name>/src/main/java/io/sbk/driver/<Name>/<Name>Writer.java |
The Writer<T> impl. |
drivers/<name>/src/main/java/io/sbk/driver/<Name>/<Name>Reader.java |
The Reader<T> impl. |
drivers/<name>/src/main/java/io/sbk/driver/<Name>/<Name>Config.java |
POJO holding driver-specific config. Bound from the properties file by Jackson. |
drivers/<name>/src/main/resources/<config-file>.properties |
Default values for config fields. The filename must exactly match the storage class’s resource lookup; existing drivers use both lowercase and class-case names. |
Driver discovery is by simple class name, case-insensitive:
-class minio resolves to io.sbk.driver.MinIO.MinIO. The class name
must match the file/directory name (modulo case).
@Synchronized, @SuppressFBWarnings, etc.).
See lombok.config.if blocks must have braces.@param, @return, @throws.checkstyle/import-control.xml.
If a new dependency brings in a new top-level package, add it there.synchronized blocks or Lock use in the driver hot path.
The harness’s lock-free property depends on the driver also not
blocking. The vendor SDK is fine; your wrapper code shouldn’t add
synchronization.TimeStamp per record; your driver should not
allocate big arrays or maps per writeAsync() call.This policy applies to all software agents and integrations working in this repository, including Codex, Devin, Windsurf, Cursor, GitHub Copilot, Claude Code, Continue, Aider, and future tools. Agent-specific configuration may add guidance, but it must not weaken or bypass this policy.
The following are latency-critical hot paths:
sbk-api per-record writer and reader loops, driver-call adapters, and
benchmark measurement submission;sbk-gem is orchestration and lifecycle code; it does not execute the
per-record measurement path. Optimize its bounded startup, SSH, diagnostics,
and shutdown behavior for reliability, but do not describe those paths as
measurement hot paths. The remote SBK processes launched by SBK-GEM remain
subject to the sbk-api and PerL rules above.
Keep the successful-operation path to the minimum work required to submit, transport, aggregate, and record the measurement. Do not add new or redundant per-operation conditional, coordination, state, or dispatch work to these hot paths. In particular, do not add:
if, switch, ternary, short-circuit, state-polling, or other conditional
branches, including extra conditions in an existing hot loop;volatile coordination fields;VarHandle, memory-fence, acquire/release, or other explicit memory-ordering
operations;synchronized, Lock, semaphore, or other contended
coordination; orwait, await, sleep, park, or
blocking-queue operations.Also minimize the non-coordination cost of every hot iteration:
Java source-level method count alone is not a performance metric: the JIT can inline small monomorphic methods and eliminate locals. Agents must inspect the actual call site and measure it rather than mechanically merging methods or removing useful locals. The concern is additional work that remains in the compiled hot path, especially dynamic dispatch, failed inlining, duplicated loads, conversions, allocation, and enlarged live state.
Existing concurrency primitives that are required by a proven algorithm are
not authorization to add more, and must not be removed mechanically. For
example, PerL’s multi-producer queue requires its existing publication
VarHandle/CAS and memory-ordering protocol. Changing such a primitive requires
a memory-model correctness argument plus queue stress, Lincheck, jcstress, GC,
and before/after performance evidence.
Keep EOF, disk-full, error handling, shutdown, lifecycle coordination, logging, and configuration decisions outside the per-record measurement and queue paths. Prefer existing exception propagation and worker/future lifecycle boundaries. Moving a check into a helper does not make it acceptable if the helper is still invoked for every record, enqueue, dequeue, or measurement. Prefer startup specialization, separate duration/fixed-record implementations, and existing empty-queue or lifecycle slow paths over a mode check in every iteration. Do not duplicate a hot loop merely on intuition: require a focused benchmark showing that specialization removes measurable compiled-path cost.
An exception to this rule requires explicit confirmation from a human developer before editing the hot path. Before requesting confirmation, the agent must:
VarHandle
operation, mutex, wait, allocation, state, or non-inlined invocation;A general request to implement a feature is not confirmation for hot-path overhead. After explicit approval, keep the addition minimal, document the reason in code, run the agreed performance comparison, and report the measured delta. Use JMH for isolated costs and a representative SBK/PerlBench workload for end-to-end throughput and latency; report allocation and variance where applicable. Passing functional tests alone is not sufficient verification.
FileTabCharacter).RegexpHeader).
Copy from any existing driver.master.halodb driver build failureThe HaloDB artifact is hosted on GitHub Packages, which routinely
exceeds its bandwidth quota. The build will fail with a 405 / 403 from
maven.pkg.github.com unless valid credentials are in
~/.gradle/gradle.properties. For agent work, halodb is currently
commented out in
settings-drivers.gradle and
build-drivers.gradle.
Do not re-enable it without confirming the user wants to deal with the
GitHub Packages credentials.
When introducing a new driver subdirectory, you must edit:
settings.gradle — no, you don’t. The driver projects are
listed in settings-drivers.gradle.settings-drivers.gradle — add
include 'drivers:<name>'.build-drivers.gradle — add
api project(':drivers:<name>') so the driver is bundled into the
installDist distribution.Forgetting either of these is the #1 source of “I added the driver
but -class <name> doesn’t find it” issues.
drivers/minio/build.gradle uses
io.minio:minio:8.5.17. Do not upgrade to 9.x without testing — the
9.x SDK sends an x-amz-sdk-checksum-algorithm header on every
PutObject that older S3 backends (Dell ECS / ObjectScale, older Ceph
RGW) reject with HTTP 400 InvalidRequest. The comment in the
build.gradle explains.
The bin/sbk script puts only the versioned pathing JAR and main SBK JAR
on the classpath; everything else (your driver’s vendor SDK, transitive
deps) is reached through the pathing jar’s Class-Path: manifest. The
pathingJar task declares runtimeClasspath as an input, so dependency graph
changes invalidate the manifest during incremental builds.
If files under an existing distribution were manually changed or copied, regenerate it with:
./gradlew clean :pathingJar installDist --rerun-tasks
The symptom of a manually inconsistent distribution is NoClassDefFoundError
for a class whose JAR is present under build/install/sbk/lib/ but absent from
the pathing manifest.
checkstyle/import-control.xml
The checkstyle rule ImportControl enforces an allow-list of top-level
packages. If your new driver pulls in a vendor SDK at a new
top-level package (e.g. software.amazon, org.apache.solr,
okhttp3), you must add
<allow pkg="package.name" /> to
checkstyle/import-control.xml
or checkstyleMain will fail.
docs/If you edit Mermaid diagrams in docs/sbk-internals.md,
test them with mmdc (mermaid-cli v11+ on Node 18+). Common pitfalls:
[ / < are rendered literally in some
versions. Use plain ASCII inside [" "] node labels.++ is a reserved token in sequence-diagram messages. Use
increment count instead.—) and Unicode arrow (→) inside sequence-diagram messages
cause parse errors. Use -- and then/->.participant X as Some (Name) with unquoted parens fails — quote the
alias: participant X as "Some (Name)".InterruptedIOExceptionWhen the benchmark duration expires, the SBK framework tears down the
SDK’s HTTP dispatcher mid-call. Your driver should treat
InterruptedIOException and RejectedExecutionException as clean
shutdowns, not errors. The MinIO driver
(MinIOWriter.java)
shows the pattern.
The default Writer<T> and Reader<T> interface methods record
start/end timestamps and call perlChannel.send(...) for you. A
driver’s writeAsync(data) only needs to perform the operation; the
harness handles timing. Do not add your own System.nanoTime()
calls in the hot path unless you have a specific reason — and if you
do, document it in the driver’s README.
| Topic | Read |
|---|---|
| End-user manual | README.md |
| Internal design / why SBK is fast / Mermaid diagrams | docs/sbk-internals.md |
| Step-by-step recipes (add a driver, add a logger, debug failures) | docs/AGENT_RECIPES.md |
| Driver spec template for spec-driven development | docs/DRIVER_SPECIFICATION.md |
| Original design papers | docs/sbk.pdf, docs/sbp.pdf, docs/sbk-slc.pdf |
This repository works equally well for both styles of AI-assisted development:
For quick fixes, single-file edits, debugging:
AGENTS.md + the relevant
AGENT_RECIPES.md recipe../gradlew :drivers:<name>:check.Loop is small. No spec document. Suitable for: bugfixes, logging tweaks, small refactors, README updates.
For larger work (a new driver, a new feature in sbk-api, a new logger
backend):
AGENTS.md + AGENT_RECIPES.md.Loop is larger but produces auditable artefacts.
The spec template explicitly cross-references the recipes, so the agent has a single deterministic path from spec → working code.
The following actions require explicit user confirmation for every specific action (not blanket approval):
git push, git tag, or any operation that publishes to a
remote.LICENSE file.gradle.properties or in the
root build.gradle.perl/, sbm/, etc.). New drivers under drivers/ are fine.halodb (see §4.1).VarHandle operation, mutex, wait, allocation,
bookkeeping state, or non-inlined dispatch to an sbk-api, PerL, SBM, or
driver hot path. The agent must first give the latency warning and obtain the
specific developer confirmation required by §3.For everything else inside drivers/, sbk-web-console/, sbk-api/, perl/, sbm/,
docs/, and the build files, normal edit-and-verify flow is fine.
Before making any change, the agent should be able to answer these questions for the change at hand. If the agent can’t answer them, it should re-read this file and the relevant linked docs.
./gradlew :<module>:check command)?checkstyle/import-control.xml if I added a new
top-level dependency package?settings-drivers.gradle and build-drivers.gradle?synchronized blocks, etc.)?VarHandle operation, mutex, wait,
allocation, bookkeeping variable, conversion, clock read, or dynamic method
invocation to a writer, reader, measurement, enqueue, dequeue, PerL, or SBM
hot path? If so, stop, warn the developer, and obtain explicit confirmation
before editing.When in doubt, prefer reading existing code over making assumptions. This codebase has more than 50 driver implementations; any specific pattern you need has almost certainly been done before.