Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions .agents/tasks/elastic-hashing-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,14 +632,66 @@ via the KSP pipeline (DP-7); pluggable hashers + good default finalizer + per-le
optional JVM-actual SIMD scan behind a capability flag + feature test (authoritative path
stays SWAR); call-site-`equals` JVM optimization.

### Phase 6 — Validation & release
### Phase 6 — Validation (the comparative matrix)

**Goal:** prove the memory/speed story against the standard library *and* the
specialist primitive-map libraries, with reproducible, honestly-framed numbers.

**Status — IMPLEMENTED** (branch `phase-6`, PR #11); details in
[`phase-6-validation-release.md`](phase-6-validation-release.md). Green on JVM (build +
Dokka + the jmh compile); reviewed by `kotlin-engineer`, `spine-code-review`,
`review-docs`, plus two CI review rounds (Copilot, Codex) — all findings resolved.
- The comparative JMH matrix in the raw-JMH `benchmarks-jvm` tier (JVM-only, where the
competitors live): `LongLongMap`/`IntIntMap` vs boxed `HashMap` and **fastutil
8.5.18 / HPPC 0.10.0 / Eclipse Collections 13.0.0 / Agrona 2.5.0**, over
lookup(hit/miss)/insert(presized/growing)/churn × sizes × dense/clustered keys, plus
a load-factor sweep (`LoadFactorBenchmark`, the two load-factor-tunable competitors).
Monomorphic adapters keep call sites clean; a `@Setup` correctness gate rejects any
miswired adapter.
- The JOL retained-footprint report (ratios from exact totals) and a reproducibility
runner (`run-matrix.sh`, `capture-env.sh`) bundling env metadata + fixed seeds + JSON.
- **Measured — memory is decisive and portable:** `LongLongMap` 19.4 B/entry
(**4.59×** vs `HashMap`, ~1.9× vs the competitors), `IntIntMap` 10.3 (**7.11×** /
~1.8×) — more compact than *even the specialist libraries*. **Distribution
robustness:** ours stays flat dense→clustered while fastutil/Eclipse degrade ~5–6×,
so ours is the *fastest* under adversarial keys (`fmix64` finalizer).
- **Split out to Phase 7:** the authoritative *absolute* time numbers (need pinned
hardware) and Maven Central publication — both human-gated separate jobs, so
validation lands without waiting on hardware or credentials.

**Goal:** publish with numbers that survive external scrutiny.

**Deliverables:** full benchmark matrix (the §1.4 grid) vs `HashMap` + fastutil/HPPC +
Eclipse + Agrona at LF 0.5/0.75/0.9/0.95/0.99; memory footprint; reproducibility
metadata (JDK/OS/CPU pinned, seeds committed, JSON outputs); README with the honest,
baseline-qualified positioning; Maven Central publication.
**Deliverables**
- The §1.4 comparative grid vs `HashMap` + fastutil/HPPC/Eclipse/Agrona; memory
footprint; reproducibility metadata (seeds committed, JSON outputs); README with the
honest, baseline-qualified positioning.

**Decision points:** none open. Absolute-number authority and publication moved to
Phase 7.

### Phase 7 — Authoritative benchmarks & release

**Goal:** publish numbers that survive external scrutiny, and ship the artifacts.

**Status — PLANNED.** Two human-gated jobs, intentionally split out of the Phase 6 PR
(configuring a pinned box and publishing are separate jobs from writing the harness):
- **Authoritative pinned-hardware benchmark run** — the full time matrix on a *pinned*
Linux box (performance governor, turbo off, `taskset`-isolated cores; Apple Silicon
cannot be pinned, so its numbers stay "indicative"). `run-matrix.sh <label> <cores>`
already has the pinned mode; the step-by-step runbook — configuring the box and
handing results back to Claude Code — is
**[issue #12](https://github.com/SpineEventEngine/elastic/issues/12)**. Folds the
pinned ns/op numbers into the README, replacing the "indicative" caveat. Memory is
already authoritative (JOL is deterministic) and needs no rerun.
- **Maven Central publication** — irreversible; needs a non-SNAPSHOT version, a claimed
namespace, a Portal token, and a signing key, and is *not* a Spine-convention
destination (so it needs explicit wiring first). Runbook:
[`../../docs/publishing.md`](../../docs/publishing.md).

**Deliverables:** the pinned-hardware benchmark matrix committed under
`benchmarks-jvm/results/<label>/` with governor/turbo/pinned-cores provenance; README
updated to the authoritative absolute numbers; Maven Central publication.

**Decision points:** whether the authoritative run also covers a Kotlin/Native tier
(DP-2) — decided when the run is scheduled.

---

Expand Down
127 changes: 127 additions & 0 deletions .agents/tasks/phase-6-validation-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Phase 6 — Validation & release

**Status: IMPLEMENTED (validation harness + docs); release is human-gated.**
Branch `phase-6`. Green on JVM (the new benchmark module compiles and runs; the
footprint report and JMH matrix produce results and JSON). Part of the phased
[implementation plan](elastic-hashing-implementation-plan.md); see also
[`docs/benchmarking.md`](../../docs/benchmarking.md) and
[`docs/publishing.md`](../../docs/publishing.md).

## Goal

Publish with numbers that survive external scrutiny: the full comparative benchmark
matrix versus the standard library and the specialist primitive-map libraries,
memory footprint, reproducibility metadata, honest baseline-qualified positioning,
and the Maven Central publication runbook.

## What shipped

### The comparative matrix (`benchmarks-jvm`, raw JMH — JVM-only)

The four competitor libraries are JVM-only, so the matrix lives in the raw-JMH tier,
never on the published `elastic` module. Verified coordinates, all on the Java 17
toolchain: **fastutil 8.5.18**, **HPPC 0.10.0**, **Eclipse Collections 13.0.0**
(`-api` + impl), **Agrona 2.5.0**.

- `CompetitorAdapters.kt` — a `LongLongCell` / `IntIntCell` interface presenting our
map, boxed `HashMap`, and the four competitors behind one monomorphic type, each
pre-sized **in its own units** (the Phase 1 fairness rule). Handles the per-library
quirks: fastutil's settable `defaultReturnValue`, HPPC's fixed-0 absent value,
Eclipse's `void put` / `removeKey` / `java.util.Map`-free API, Agrona's mandatory
`missingValue` sentinel and its `java.util.Map`-implementing `size` **property**.
- `LongLongMatrixBenchmark` / `IntIntMatrixBenchmark` — `@Param impl × size × dist`
over ops `lookupHit` (random-access), `lookupMiss`, `insertPresized`,
`insertGrowing`, `churn`. A **correctness gate in `@Setup`** (the hit-sum must equal
the key-sum) refuses to benchmark a miswired adapter. **No `iterate` op**: our
primitive-value maps expose no non-boxing traversal (DP-10 lean surface), so a fair
head-to-head cannot include it — the gap is a tracked follow-up.
- `LoadFactorBenchmark` — lookup cost vs load factor `{0.50…0.99}` for the two
competitors that can be *held* at a high load (fastutil, HPPC; their LF tunes to
0.99). Ours (fixed `7/8`) and `HashMap` (`0.75`) cannot be held at an arbitrary load
and are fixed reference points; Agrona is excluded (its LF is capped at 0.9, so it
cannot reach the ≥0.95 points — a limitation the pre-merge smoke caught). The ≈0.99
regime belongs to the paper structures, gated by their own probe-count specs.
Capacity is pinned at `2^20` and the fill count chosen to hit the target load; every
invocation does the same `2^20` lookups, so scores are load-comparable.
- `MatrixKeys.kt` — the `DENSE` and `CLUSTERED` key sets (the latter mandated by the
fairness gate) and the fixed-seed shuffle. The `Int` `CLUSTERED` stride is milder
than the `Long` one because a hard-collision key set is not constructible in-range
for 32-bit keys at a million entries.

### The footprint report (JOL)

`FootprintReport.kt` (`main()` in the jmh source set, `./gradlew
:benchmarks-jvm:footprintReport`) measures the exact retained heap of every map, built
raw (no adapter wrapper), pre-sized in its own units at `n = 229_376` (the `7/8` load
of a `2^18` table), and writes Markdown + TSV. Deterministic; the `Long→Long` figure
reproduces the committed Phase 1 measurement exactly.

### Reproducibility harness

- `capture-env.sh` — JDK/OS/CPU/RAM and, critically, whether CPU frequency was pinned;
portable across macOS (dev) and Linux (CI).
- `run-matrix.sh <label>` — captures the environment, runs the footprint report and
the authoritative JMH matrix (`@Fork 3`, `5×1s` + `5×1s`), and collects a
self-describing bundle under `benchmarks-jvm/results/<label>/`.
- Seeds are fixed (seed 42) and shared across maps; documented.
- Committed indicative bundle: `benchmarks-jvm/results/m4max-indicative/`.

## Results (measured)

- **Memory — decisive, and the strongest portable result.** Retained B/entry at
`n = 229_376` (ratios from exact retained totals): `LongLongMap` **19.4** (**4.59×**
smaller than `HashMap` 89.1, and **~1.9×** smaller than the four competitors, all
36.6); `IntIntMap` **10.3** (**7.11×** smaller than `HashMap` 73.1, **~1.8×** smaller
than the competitors, all 18.3). Ours is more compact than *even the specialist
primitive libraries*, from packing key+value+1 control byte at `7/8` load.
- **Time — distribution robustness is the headline** (qualitative; absolute ms are
hardware-specific and deferred to a pinned run). `1M` `lookupHit`, M4 Max, indicative:
`OURS` is flat across `DENSE` (9.9 ms) → `CLUSTERED` (10.2 ms), while `FASTUTIL`
degrades ~5× (7.7 → 36 ms) and `ECLIPSE` ~6× (2.0 → 13 ms). On dense keys Eclipse's
weak spread wins (contiguous, cache-resident); on adversarial keys our `fmix64`
finalizer makes `OURS` the fastest. A dense-only view would have inverted the
conclusion — the fairness gate's adversarial mandate earned its keep.
- **vs `HashMap` (the committed gate):** met — faster at scale on lookup and presized
insert, and far more compact, per `performance-goals.md`.

## Split out to Phase 7 (authoritative benchmarks & release)

Two human-gated jobs were moved out of this phase so the validation work could land
without waiting on hardware or credentials — they are separate jobs (configuring a
pinned box, publishing) from writing the harness. See the plan's **Phase 7**.

- **Authoritative pinned-hardware benchmark run** — the absolute time numbers need a
*pinned* Linux box (Apple Silicon cannot be pinned). `run-matrix.sh <label> <cores>`
has the `taskset` pinned mode; the step-by-step runbook is
**[issue #12](https://github.com/SpineEventEngine/elastic/issues/12)**. Memory is
already authoritative and needs no rerun.
- **Maven Central publication** — deliberately not executed: irreversible, and needs a
signing key, a Portal token, a claimed `io.spine` namespace, a **non-SNAPSHOT**
version, and explicit wiring (it is not a Spine-convention destination). Runbook:
[`docs/publishing.md`](../../docs/publishing.md); the `elastic` module registers no
publication tasks yet, so wiring them (safe to validate locally with
`publishToMavenLocal`) is the first release task.

## Decisions

- **Competitors are a reference ceiling, not a success gate.** fastutil/HPPC are
co-fastest among classic open-addressing libs; we do not claim to beat them on raw
dense lookup. The committed baseline is the standard library. Differentiators:
memory, distribution robustness, multiplatform reach.
- **Competitor libs confined to `benchmarks-jvm/jmh`** — benchmark-only, JVM-only,
never a dependency of the published module. Versions pinned inline (not the
`io.spine.dependency.*` catalog) as one-off measurement peers.
- **Absolute time numbers are not committed as headline figures** — only the
ratios/robustness that survive the unpinned-hardware caveat. Authoritative absolute
numbers require a pinned-hardware `run-matrix.sh` run.

## Open follow-ups

- Non-boxing `forEach` on the primitive-value maps (closes the `iterate` gap; a chip
was spawned during this phase).
- Kotlin/KSP pin (2.3.21) no longer has a rationale (codegen is template-based); an
upgrade is a separate task.

The two release-gated jobs — the pinned-hardware authoritative run
([issue #12](https://github.com/SpineEventEngine/elastic/issues/12)) and Maven Central
publication — are **Phase 7**, covered in *Split out to Phase 7* above.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,24 @@ detail.
- Opt-in boxed `MutableMap` views: `asMutableMap()` on the boxed-value maps
(fail-fast iteration) and on `SingleWriterSwissLongMap` (weakly consistent
snapshot iteration).
- **Phase 6 — planned.** Validation and release: the full benchmark matrix on
pinned hardware, reproducibility metadata, and Maven Central publication.
- **Phase 6 — validation harness complete; release is human-gated.** The
comparative benchmark matrix versus the standard library and the specialist
primitive-map libraries (fastutil, HPPC, Eclipse Collections, Agrona), the
exact-footprint report, and the reproducibility harness are in place; see
[`docs/benchmarking.md`](docs/benchmarking.md). Two measured headlines:
- **Memory:** `LongLongMap` retains **19.4 B/entry** — **4.59×** less than boxed
`HashMap<Long, Long>` (89.1) and **~1.9× less than every primitive competitor**
(36.6 B/entry each); `IntIntMap` is **10.3 B/entry** (**7.11×** vs `HashMap`,
**~1.8×** vs competitors at 18.3 B/entry). Ratios are from exact JOL retained
totals.
- **Distribution robustness:** on out-of-cache random lookup our `fmix64`
finalizer keeps `LongLongMap` flat between dense and adversarial (clustered)
keys, where fastutil degrades ~5× and Eclipse ~6× — so ours is the *fastest*
under adversarial keys.

Maven Central publication is deliberately deferred (it is irreversible and needs
signing keys, a Portal token, and a non-SNAPSHOT version); the runbook is
[`docs/publishing.md`](docs/publishing.md).

## Modules

Expand Down
51 changes: 51 additions & 0 deletions benchmarks-jvm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import io.spine.dependency.test.Jol
import io.spine.gradle.report.license.LicenseReporter

// The raw-JMH second benchmark tier: JVM-only benchmarks written against JMH's
Expand Down Expand Up @@ -53,9 +54,59 @@ kotlin {
jvmToolchain(17)
}

// Competitor primitive-map libraries, benchmarked as a *reference ceiling* for the
// Phase 6 matrix (see `docs/benchmarking.md`). These are JVM-only and benchmark-only:
// they are on the `jmh` source set, never on the published `elastic` module. Versions
// are pinned here (not in the `io.spine.dependency.*` catalog) because they are
// one-off measurement peers, not project dependencies. All four run on the Java 17
// toolchain. Verified coordinates (mid-2026):
// - fastutil it.unimi.dsi:fastutil:8.5.18
// - HPPC com.carrotsearch:hppc:0.10.0
// - Eclipse Collections org.eclipse.collections:eclipse-collections(-api):13.0.0
// - Agrona org.agrona:agrona:2.5.0
val fastutilVersion = "8.5.18"
val hppcVersion = "0.10.0"
val eclipseCollectionsVersion = "13.0.0"
val agronaVersion = "2.5.0"

dependencies {
// The KMP project dependency resolves to its JVM variant here.
jmh(project(":elastic"))

// Reference-ceiling competitors for the comparative matrix.
jmh("it.unimi.dsi:fastutil:$fastutilVersion")
jmh("com.carrotsearch:hppc:$hppcVersion")
jmh("org.eclipse.collections:eclipse-collections-api:$eclipseCollectionsVersion")
jmh("org.eclipse.collections:eclipse-collections:$eclipseCollectionsVersion")
jmh("org.agrona:agrona:$agronaVersion")

// JOL (Java Object Layout) for the exact retained-footprint report; see the
// `footprintReport` task below. Test/benchmark-scoped, never published.
jmh(Jol.lib)
}

// ---------------------------------------------------------------------------
// Retained-footprint report (JOL).
//
// A plain `main()` in the `jmh` source set (so it shares the competitor
// classpath) that measures the exact retained heap of every map in the matrix
// with JOL and writes a Markdown + TSV table under `build/reports/footprint/`.
// This is the authoritative *memory* half of the Phase 6 matrix — deterministic,
// fast, and run with `./gradlew :benchmarks-jvm:footprintReport`. The time half
// is the JMH benchmarks in this module.
// ---------------------------------------------------------------------------
val footprintReport by tasks.registering(JavaExec::class) {
group = "verification"
description = "Writes the retained-heap footprint matrix (ours vs stdlib + competitors)."
val jmhSourceSet = sourceSets["jmh"]
classpath = jmhSourceSet.runtimeClasspath
mainClass.set("io.spine.elastic.benchmark.jmh.FootprintReportKt")
argumentProviders.add(
CommandLineArgumentProvider {
val out = layout.buildDirectory.dir("reports/footprint").get().asFile
listOf(out.absolutePath)
},
)
}

jmh {
Expand Down
Loading
Loading