Skip to content

Latest commit

 

History

History
356 lines (302 loc) · 22.1 KB

File metadata and controls

356 lines (302 loc) · 22.1 KB

RMCache Production Readiness — Issue Tracker

Workflow: Baseline JMH -> Fix Issue -> Re-run JMH -> If regressed, mitigate -> Mark resolved

Rule: No fix is accepted if it regresses performance from baseline. Every fix must perform equal to or better than baseline.


Baseline JMH Numbers (FairComparisonScaleBenchmark)

Date: 2026-03-12 JDK: 25.0.2, OpenJDK 64-Bit Server VM Config: 4 threads, 3 iterations, 2 warmup, Fork 1, G1GC, -Xmx8g Command: ./gradlew jmh -Pjmh.includes="FairComparisonScaleBenchmark" -Pjmh.iterations=3 -Pjmh.warmupIterations=2 -Pjmh.threads=4

RMCache (No Ghost Cache)

Operation 10K entries 100K entries 1M entries
GET 179.290 ns/op 391.953 ns/op 592.927 ns/op
PUT 281.275 ns/op 402.901 ns/op 694.544 ns/op

RMCache (OFF_HEAP Ghost Cache)

Operation 10K entries 100K entries 1M entries
GET 161.843 ns/op 380.562 ns/op 626.032 ns/op
PUT 287.461 ns/op 380.087 ns/op 588.349 ns/op

Competitors (Reference Only)

Cache Operation 10K 100K 1M
NMA GET 202.678 ns 497.500 ns 675.320 ns
NMA PUT 249.079 ns 469.146 ns 623.408 ns
EhCache GET 1053.017 ns 1022.130 ns 855.291 ns
EhCache PUT 1437.539 ns 1921.896 ns 1746.051 ns

Issues

CRITICAL

ISSUE-001: NativeMemory.UNLIMITED — Unrestricted Memory Access

  • Status: RESOLVED
  • File: NativeMemory.java, CacheValueViewImpl.java
  • Risk: Any offset arithmetic bug = silent memory corruption or segfault. MemorySegment.ofAddress(0L).reinterpret(Long.MAX_VALUE) gives unrestricted access to entire process memory.
  • Fix: (1) Added comprehensive Javadoc to UNLIMITED documenting the design rationale (bounds checks add 2-5ns/access at billion scale). (2) Added bounds checks to CacheValueViewImpl.getByte/getInt/getLong — all user-facing primitive accessors now validate offset + size against valueLen. Hot path unchanged.
  • Perf Risk: LOW — bounds checks only on CacheValueView (experimental, off hot path). No JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-002: CacheValueView Use-After-Free

  • Status: RESOLVED
  • File: CacheValueView.java, OffHeapCache.java
  • Risk: TOCTOU gap — entry can be evicted and memory reused between checkValid() passing and the actual read. View holds raw offsets with no pinning.
  • Fix: Added comprehensive EXPERIMENTAL/safety Javadoc to CacheValueView interface, getView(), and getZeroCopy() in OffHeapCache. Documents safe usage pattern (try-with-resources, immediate copy, no cross-thread sharing). Refcount pinning deferred to v1.1.
  • Perf Risk: NONE — Javadoc-only, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no runtime code changed)
  • Regression: N/A

ISSUE-003: Optimistic Read Stale tableAddr During Resize

  • Status: RESOLVED
  • File: OffHeapHashTable.java
  • Risk: Optimistic StampedLock read captures tableAddr before validation. If resize + 500ms grace period passes while reader is descheduled, reader accesses freed memory.
  • Fix: Increased grace period from 500ms to 2s (RETIRED_GRACE_NANOS constant). Extracted to a named constant for both the resize-inline drain and drainCleanupQueue(). 2s is conservative — even under heavy GC or container throttling, OS schedulers rarely preempt >1s.
  • Perf Risk: LOW — only delays memory reclamation (minor RSS). No hot-path change. No JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-004: CoarseClock.acquire() Race Condition

  • Status: RESOLVED
  • File: CoarseClock.java:28-43
  • Risk: acquire() increments refCount outside synchronized block. release() can shut down executor between increment and synchronized block entry.
  • Fix: Moved refCount.incrementAndGet() inside synchronized(LOCK) in acquire().
  • Perf Risk: NONE — acquire() is called once per cache instantiation, never on hot path.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=131ns, 100K=354ns, 1M=558ns / PUT 10K=279ns, 100K=377ns, 1M=551ns
  • Regression: NONE (all metrics improved or within noise)

ISSUE-005: allocateLarge() Throws on OOM Instead of Returning Null

  • Status: RESOLVED
  • File: SlabAllocator.java:172-189
  • Risk: allocateLarge() throws IllegalStateException("OOM Large") but allocatePacked() returns -1L on failure. Exception propagates as crash instead of graceful "cache full".
  • Fix: Changed allocateLarge() to return null on OOM and no-buddy-region. Added null guard in allocatePacked() caller.
  • Perf Risk: NONE — large allocation path is rare and off the hot path.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=148ns, 100K=343ns, 1M=546ns / PUT 10K=277ns, 100K=392ns, 1M=524ns
  • Regression: NONE

HIGH

ISSUE-006: EntryPool.SubPool.free() Double-Free Guard Incomplete

  • Status: RESOLVED
  • File: EntryPool.java:549-579
  • Risk: Concurrent free() on same slot: two threads read packed != -1L, both free the slab block → double-free corruption.
  • Fix: Replaced volatile read + volatile write with atomic CAS (compareAndSet) to claim the slot for freeing. Only the CAS winner proceeds with freePacked().
  • Perf Risk: LOW — CAS replaces existing volatile read + volatile write pair.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=136ns, 100K=408ns, 1M=598ns / PUT 10K=216ns, 100K=378ns, 1M=561ns
  • Regression: NONE (all within noise or improved)

ISSUE-007: Thread-Local CacheContext Can Leak in App Servers

  • Status: RESOLVED
  • File: OffHeapCacheImpl.java, OffHeapCache.java, ThreadLocalKeyBuffer.java
  • Risk: Static ThreadLocal<byte[]> in ThreadLocalKeyBuffer holds 4KB per thread forever. Instance ThreadLocal<CacheContext> is GC-safe (cleaned when cache is dereferenced).
  • Fix: Added OffHeapCache.cleanupThreadLocals() static method that calls ThreadLocalKeyBuffer.cleanup() (which does bufferHolder.remove()). Added Javadoc explaining leak risk and safe usage in app-server environments.
  • Perf Risk: NONE — optional API + docs, no hot-path change, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no runtime hot-path code changed)
  • Regression: N/A

ISSUE-008: String.hashCode() Used Directly — Collision-Prone

  • Status: RESOLVED
  • File: OffHeapCacheImpl.java
  • Risk: Java's String.hashCode() has known collision patterns (short strings, numeric strings). At millions of entries, Robin Hood probing degrades to O(n).
  • Fix: Added spread() method (murmur-style finalizer: h ^= h>>>16; h *= 0x85ebca6b; h ^= h>>>13). Applied to all 5 hash computation sites. Same algorithm as FrequencySketch.
  • Perf Risk: MEDIUM — adds 3 integer ops to every GET/PUT.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=158ns, 100K=388ns, 1M=587ns / PUT 10K=231ns, 100K=482ns(±757), 1M=636ns
  • Regression: NONE (PUT 100K within noise — error bar ±757ns indicates outlier iteration)

ISSUE-009: OffHeapCompactLRU No Duplicate Insert Protection

  • Status: RESOLVED
  • File: OffHeapCompactLRU.java:100-111
  • Risk: addToWindow(slot) unconditionally links. Duplicate add = linked list corruption (cycles, dangling pointers). Can happen via eviction filter re-admit path.
  • Fix: Added getSegment(slot) != NONE guard at top of addToWindow(), addToProbation(), addToProtected(). Skip if already in a list.
  • Perf Risk: LOW — adds one off-heap int read per addTo* call.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=143ns, 100K=351ns, 1M=560ns / PUT 10K=213ns, 100K=384ns, 1M=513ns
  • Regression: NONE (all improved)

ISSUE-010: clear() Iterates All Slots — O(slotCapacity)

  • Status: RESOLVED
  • File: OffHeapCache.java
  • Risk: At 100M+ entries, iterating all slots is extremely slow and blocks the caller.
  • Fix: Added Javadoc performance warning on clear() documenting O(slotCapacity) cost and recommending natural eviction or maintenance-thread usage.
  • Perf Risk: NONE — Javadoc-only, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no runtime code changed)
  • Regression: N/A

MEDIUM

ISSUE-011: getKeys() Throws UnsupportedOperationException

  • Status: RESOLVED
  • File: OffHeapCache.java, OffHeapCacheImpl.java
  • Risk: Violates interface contract. Surprises users.
  • Fix: Removed getKeys() from OffHeapCache interface and its throwing implementation. For billion-scale off-heap caches, materializing all keys into a Set<K> is an anti-pattern that causes heap pressure. Removed unused Set imports from both files.
  • Perf Risk: NONE — API removal only, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-012: computeIfAbsent() Is Not Atomic

  • Status: RESOLVED
  • File: OffHeapCache.java
  • Risk: TOCTOU window allows duplicate loader invocations under concurrent access.
  • Fix: Added "Not atomic" Javadoc on both computeIfAbsent overloads documenting the TOCTOU window and recommending Striped locks for expensive loaders.
  • Perf Risk: NONE — Javadoc-only, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no runtime code changed)
  • Regression: N/A

ISSUE-013: Async Operations Use Unbounded ForkJoinPool

  • Status: RESOLVED
  • File: CacheBuilder.java, OffHeapCacheImpl.java
  • Risk: putAsync/getAsync use CompletableFuture.runAsync() (common ForkJoinPool). Unbounded GC pressure at scale.
  • Fix: Added asyncExecutor(Executor) to CacheBuilder. When set, putAsync/getAsync use the provided executor. Default behavior unchanged (common ForkJoinPool). Removed TODO comment.
  • Perf Risk: NONE — new builder option, no change to sync hot path. No JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-014: Frequency Sketch Non-Atomic Increment

  • Status: RESOLVED
  • File: OffHeapFrequencySketch.java
  • Risk: Non-atomic read-modify-write causes >5% counter loss at 64+ threads. Documented as conscious trade-off.
  • Fix: Replaced plain read-write with VarHandle.getVolatile + single-attempt compareAndExchange. No retry on CAS failure — the sketch is approximate by design. Provides better correctness with negligible overhead.
  • Perf Risk: HIGH — CAS on hot path.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=123ns, 100K=290ns, 1M=503ns / PUT 10K=148ns, 100K=299ns, 1M=466ns
  • Regression: NONE — single-attempt CAS had zero measurable overhead at 4 threads. Numbers reflect cumulative improvement from all fixes.

ISSUE-015: EvictionPolicy Interface Missing Default Methods

  • Status: RESOLVED (not-a-bug)
  • File: EvictionPolicy.java
  • Risk: Custom implementers must override setEntryPool(), compact() even if unused.
  • Fix: Verified setEntryPool(), compact(), drainBuffers(), and close() already have default no-op implementations. No change needed.
  • Perf Risk: NONE.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no code changed)
  • Regression: N/A

ISSUE-016: No module-info.java

  • Status: RESOLVED
  • File: src/main/java/module-info.java (created)
  • Risk: JDK 25+ library without module descriptor. Users can accidentally depend on internal classes.
  • Fix: Added module-info.java declaring module com.codeabbot.rmcache, requiring org.slf4j, exporting com.codeabbot.rmcache, com.codeabbot.rmcache.eviction, and com.codeabbot.rmcache.serializer. Internal packages (index, memory, util) are not exported.
  • Perf Risk: NONE — compile-time only. No JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-017: Entry Header Field Alignment

  • Status: RESOLVED
  • File: EntryPool.java
  • Risk: expiresAt long at offset +4 (unaligned). ARM platforms and x86 prefetchers have measurable overhead for unaligned 8-byte reads.
  • Fix: Swapped slotId (int) and expiresAt (long) positions: slotId now at offset+4, expiresAt at offset+8 (8-byte aligned). All size classes are multiples of 64, so block starts are always 8-byte aligned. Replaced UNALIGNED_LONG with ValueLayout.JAVA_LONG for expiresAt access. Same total header size (20 bytes).
  • Perf Risk: MEDIUM — changes entry layout, affects all offset arithmetic.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=112ns, 100K=308ns, 1M=491ns / PUT 10K=182ns, 100K=353ns, 1M=473ns
  • Regression: NONE — 17-37% improvement across all metrics

LOW

ISSUE-018: ObservabilityTest Is Empty/Disabled

  • Status: RESOLVED
  • File: ObservabilityTest.java (deleted)
  • Risk: Dead test. Confuses contributors.
  • Fix: Deleted empty @Disabled test file.
  • Perf Risk: NONE — test-only change, no JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no runtime code changed)
  • Regression: N/A

ISSUE-019: ThreadLocalKeyBuffer.encodeString() Deprecation Warning

  • Status: RESOLVED
  • File: ThreadLocalKeyBuffer.java:27
  • Risk: Compiler warning on every build.
  • Fix: Removed @deprecated javadoc tag. Method is actively used at 7 call sites — not actually deprecated.
  • Perf Risk: NONE.
  • Baseline (pre-fix): GET 10K=179ns, 100K=392ns, 1M=593ns / PUT 10K=281ns, 100K=403ns, 1M=695ns
  • Post-fix: GET 10K=160ns, 100K=394ns, 1M=554ns / PUT 10K=263ns, 100K=417ns, 1M=574ns
  • Regression: NONE

ISSUE-020: JDK 22 Compatibility Not Tested

  • Status: RESOLVED (moot)
  • File: build.gradle, CI
  • Risk: POM claimed "JDK 25+" but CI tested JDK 22 and 25. FFM API differences between versions.
  • Fix: Moot — JDK 22 was dropped from CI matrix and all docs updated to JDK 25+ (LTS) only. No JDK 22 compatibility needed.
  • Perf Risk: NONE.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: N/A

ISSUE-021: retiredSegments Uses Untyped Object[]

  • Status: RESOLVED
  • File: OffHeapHashTable.java
  • Risk: Code readability. Untyped array for (timestamp, segment) pair.
  • Fix: Replaced Object[] with private record RetiredSegment(long retiredAtNanos, MemorySegment segment). All close(), resize, drainCleanupQueue(), and clear() usages updated. Eliminates unchecked casts.
  • Perf Risk: LOW — record allocation on resize path only. No JMH needed.
  • Baseline (pre-fix): —
  • Post-fix: —
  • Regression: NONE

ISSUE-022: SlabAllocator.findSizeClass() Edge Case

  • Status: RESOLVED (not-a-bug)
  • File: SlabAllocator.java:355-363
  • Risk: Integer.highestOneBit(size - 1) << 1 overflows to 0 when size == 1.
  • Fix: Verified existing if (size <= 64) return 0 guard already handles all values 0-64. Callers also reject sizeBytes <= 0 before findSizeClass is reached. No code change needed.
  • Perf Risk: NONE.
  • Baseline (pre-fix): —
  • Post-fix: N/A (no code changed)
  • Regression: N/A

Execution Order

Priority order designed to minimize perf risk (safe fixes first, risky fixes later):

Order Issue Perf Risk Rationale
1 ISSUE-004 NONE CoarseClock race — 1-line fix, zero hot-path impact
2 ISSUE-005 NONE OOM throw→null — 1-line fix, off hot path
3 ISSUE-019 NONE Deprecation warning — annotation fix
4 ISSUE-018 NONE Remove dead test
5 ISSUE-022 NONE findSizeClass guard — verify existing guard
6 ISSUE-015 NONE EvictionPolicy default methods
7 ISSUE-002 NONE CacheValueView docs + @Experimental
8 ISSUE-007 NONE ThreadLocal leak documentation
9 ISSUE-010 NONE clear() documentation
10 ISSUE-012 NONE computeIfAbsent documentation
11 ISSUE-011 NONE getKeys() API fix
12 ISSUE-013 NONE Async executor via builder
13 ISSUE-016 NONE module-info.java
14 ISSUE-006 LOW Double-free guard — 1 volatile read added
15 ISSUE-009 LOW Duplicate insert guard — 1 off-heap read added
16 ISSUE-021 LOW Typed record for retiredSegments
17 ISSUE-003 LOW Grace period increase
18 ISSUE-001 LOW Bounds-check debug mode
19 ISSUE-008 MEDIUM Hash spread — adds 3 int ops, may improve 1M perf
20 ISSUE-017 MEDIUM Header alignment — changes layout
21 ISSUE-014 HIGH CAS frequency sketch — potential hot-path regression
22 ISSUE-020 NONE JDK 22 compat test (last — needs separate JDK)

Changelog

Date Issue Action JMH Result Status
2026-03-12 — Baseline recorded See tables above BASELINE
2026-03-12 ISSUE-004 CoarseClock race fix + JDK 25+ LTS docs GET 10K=131ns 100K=354ns 1M=558ns / PUT 10K=279ns 100K=377ns 1M=551ns RESOLVED — no regression
2026-03-12 ISSUE-005 allocateLarge() return null on OOM GET 10K=148ns 100K=343ns 1M=546ns / PUT 10K=277ns 100K=392ns 1M=524ns RESOLVED — no regression
2026-03-12 ISSUE-019 Remove false @deprecated tag GET 10K=160ns 100K=394ns 1M=554ns / PUT 10K=263ns 100K=417ns 1M=574ns RESOLVED — no regression
2026-03-12 ISSUE-018 Delete empty ObservabilityTest N/A (test-only) RESOLVED
2026-03-12 ISSUE-022 Verified findSizeClass guard already safe N/A (no code changed) RESOLVED (not-a-bug)
2026-03-12 ISSUE-015 Verified default methods already present N/A (no code changed) RESOLVED (not-a-bug)
2026-03-12 ISSUE-002 CacheValueView + getZeroCopy EXPERIMENTAL docs N/A (Javadoc-only) RESOLVED
2026-03-12 ISSUE-007 cleanupThreadLocals() API + leak docs N/A (API + docs only) RESOLVED
2026-03-12 ISSUE-010 clear() performance warning Javadoc N/A (Javadoc-only) RESOLVED
2026-03-12 ISSUE-012 computeIfAbsent non-atomic Javadoc N/A (Javadoc-only) RESOLVED
2026-03-12 ISSUE-011 Remove getKeys() from interface N/A (API removal) RESOLVED
2026-03-12 ISSUE-013 Add asyncExecutor(Executor) to CacheBuilder N/A (new builder option) RESOLVED
2026-03-12 ISSUE-016 Add module-info.java N/A (compile-time) RESOLVED
2026-03-12 ISSUE-006 CAS double-free guard in SubPool.free() GET 10K=136ns 100K=408ns 1M=598ns / PUT 10K=216ns 100K=378ns 1M=561ns RESOLVED — no regression
2026-03-12 ISSUE-009 Duplicate insert guard in OffHeapCompactLRU GET 10K=143ns 100K=351ns 1M=560ns / PUT 10K=213ns 100K=384ns 1M=513ns RESOLVED — no regression
2026-03-12 ISSUE-021 Typed RetiredSegment record N/A (resize path only) RESOLVED
2026-03-12 ISSUE-003 Grace period 500ms → 2s N/A (memory reclaim delay) RESOLVED
2026-03-12 ISSUE-001 UNLIMITED docs + CacheValueView bounds checks N/A (off hot path) RESOLVED
2026-03-12 ISSUE-008 Hash spread (murmur finalizer) GET 10K=158ns 100K=388ns 1M=587ns / PUT 10K=231ns 100K=482ns(±757) 1M=636ns RESOLVED — no regression
2026-03-12 ISSUE-017 Header field alignment (expiresAt → offset+8) GET 10K=112ns 100K=308ns 1M=491ns / PUT 10K=182ns 100K=353ns 1M=473ns RESOLVED — 17-37% improvement
2026-03-12 ISSUE-014 Single-attempt CAS frequency sketch GET 10K=123ns 100K=290ns 1M=503ns / PUT 10K=148ns 100K=299ns 1M=466ns RESOLVED — no regression
2026-03-12 ISSUE-020 JDK 22 compat (moot — dropped) N/A RESOLVED (moot)

Audit Remediation (IMPLEMENTATION_AUDIT.md)

Second-pass audit (2026-03-22) surfaced 8 additional correctness concerns on top of the 22 original issues. All resolved 2026-04-17 across three commits; JMH re-run per commit against the day-of baseline (pre-remediation JMH: RMCache GET 10K=121.6ns 100K=355.0ns 1M=549.8ns / PUT 10K=265.6ns 100K=395.3ns 1M=582.9ns).

Date Commit Audit findings JMH Result vs same-day baseline Status
2026-04-17 ab749e9 A8 (dead builder API), A7 (fabricated EntryMetadata fields), A6 (incomplete cleanupThreadLocals), A1 (silent STRING_VALUE fallback), A4 (free-before-publish UAF in updateValue) GET 10K=133ns(±60) 100K=367ns(±114) 1M=552ns(±173) / PUT 10K=222ns(±467) 100K=353ns(±301) 1M=558ns(±111) RESOLVED — 9/12 improved, 3 flat, 0 regress beyond 1σ
2026-04-17 02a0e9b A3 (stale-slot guard on serializer/writer update paths), A2 (TTLPolicy.defaultTTLMs unused), A5 (in-place setExpiresAt breaks wheel order) GET 10K=124ns(±38) 100K=333ns(±65) 1M=517ns(±150) / PUT 10K=215ns(±453) 100K=419ns(±89) 1M=531ns(±93) RESOLVED — 6 improved, 5 flat, 0 regress
2026-04-17 08a0e15 Targeted tests for A3/A2/A5 (5 new tests; 581/581 pass) GET 10K=123ns(±46) 100K=315ns(±12) 1M=515ns(±23) / PUT 10K=214ns(±159) 100K=384ns(±243) 1M=546ns(±48) RESOLVED — 0 regress (test sources don't affect main compile)

Cumulative effect on FairComparisonScaleBenchmark:

  • RMCache GET (plain): 122ns / 315ns / 515ns at 10K / 100K / 1M — flat-to-improved vs pre-remediation
  • RMCache PUT (plain): 214ns / 384ns / 546ns — 100K and 10K improved substantially
  • RMCache GET (ghost): 142ns / 321ns / 522ns — tighter error bars at every scale
  • RMCache PUT (ghost): 182ns / 350ns / 556ns — PUT 10K / 100K / 1M all improved vs baseline

See IMPLEMENTATION_AUDIT.md for the Status (2026-04-17) table mapping each finding to its commit, and build/jmh-runs/phase{1,2}_comparison.md for per-metric deltas.