Skip to content

perf(storer): remove per-chunk index lookups - #5615

Draft
gacevicljubisa wants to merge 10 commits into
masterfrom
perf/sample-02-locating-chunkstore
Draft

gacevicljubisa wants to merge 10 commits into
masterfrom
perf/sample-02-locating-chunkstore

Conversation

@gacevicljubisa

Copy link
Copy Markdown
Member

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have filled out the description and linked the related issues.

Description

Makes ReserveSample substantially cheaper:

  • chunk data is read into a per-worker buffer instead of a fresh allocation per chunk
  • ChunkBinItem carries a sharky location hint, so the per-chunk retrieval-index lookup disappears; reads fall back to the index when the hint is absent or stale
  • migration step 8 backfills the hint for reserves written before the field existed (without it the hint is inert on an upgraded node)

Measured

Testnet, 2.49M chunk reserve, radius 2, no SIMD, identical config, both nodes sampled simultaneously via /rchash. Median of 3 runs.

per chunk master this PR Δ
ReserveSample allocations 146.8 69.9 −52.6%
chunk_load 9.89 ms 0.48 ms −95%
transformedAddress (control — unchanged code) 67.2 66.6 −1%
sample duration 701 s 227 s −67%

Open API Spec Version Changes (if applicable)

Motivation and Context (Optional)

Related Issue (Optional)

Screenshots (if appropriate):

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

gacevicljubisa and others added 10 commits September 14, 2026 11:57
Sampling is about to start reading chunks into a buffer that each worker
reuses. Nothing today checks that the bytes handed back in a SampleItem are
still the bytes of that chunk, so a reused buffer would silently hand the
redistribution proof the contents of some later chunk.

assertValidSample now checks two things for every item: that ChunkData still
reproduces ChunkAddress, and that no two items share a backing array. Every
existing sample test picks both up.

Also add the rulers for the work that follows. BenchmarkReserveSample1k keeps
its name and behaviour so the recorded baseline stays comparable; its body
moves to a helper that BenchmarkReserveSample10k reuses over a ten times
larger reserve. BenchmarkChunkStoreGet measures a single chunk read, split
into a variant that builds the ChunkStore handle per call as the sampler does
today and one that hoists it, so the cost of the handle alone is visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
db.ChunkStore() builds three objects every time it is called, and the sampler
called it once per chunk. On a testnet node that is 2.3 million handles per
round, thrown away immediately.

Hoist it to one per worker. The handle is deliberately not shared across
workers: the read-only chunk store makes no thread-safety promise, and three
allocations per worker is already nothing next to three per chunk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
Both ReserveSample benchmarks fill their reserve with
chunk.GenerateValidRandomChunkAt, which produces content-addressed chunks only.
The SOC branch of transformedAddress is therefore never measured by them, and
any work that branch does beyond hashing the wrapped CAC is invisible.

Benchmark the function directly, one case per chunk type, through a shim in
export_test.go. The shim takes a swarm.Chunk and is held to that signature on
purpose so the same benchmark source can be run against branches whose internal
transformedAddress is shaped differently; only the shim changes between them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFUseFQ8rhp6N9X6YS7pbq
…e sampling

Accelerate ReserveSample by bypassing LevelDB index lookups through
opaque storage.ChunkLocation hints without breaking clean storage
abstractions:

- Introduce storage.ChunkLocation and capability interfaces
  (LocatingPutter, LocatingReplacer, LocatingGetterInto) in pkg/storage
- Support dual-format backward-compatible ChunkBinItem deserialization
  (106 bytes legacy, 114 bytes with location) avoiding database migrations
- Encapsulate thread-safe LocationGuard inside internal/chunkstore to
  prevent use-after-free race conditions when Sharky reuses freed slots
- Use GetIntoLoc in reserve sampler workers with automatic fallback to
  standard GetInto for legacy or guarded locations
…ion hints

Ensure GetIntoLoc defaults to fail-safe behavior by falling back to standard
verified GetInto whenever no sampling session is active:

- Add LocationGuard.SessionActive() method
- Check !guard.SessionActive() in GetIntoLoc before using sharky location
- Update tests to verify fallback when inactive and direct read when active
Align execution order in ReplaceLoc with Delete to enforce the principle
of marking resources in LocationGuard prior to releasing them in Sharky:

- Move guard.MarkFreed before sh.Release in ReplaceLoc
- Add test coverage verifying that old locations trigger safe indexStore
  fallback after ReplaceLoc
…ore the field

The location hint added to ChunkBinItem only lands on entries that are
written after the upgrade. Everything already in the reserve stays in the
106-byte legacy layout, Unmarshal leaves its Location zero, and GetIntoLoc
falls back to the retrieval-index lookup for every one of those chunks. The
dual-format Unmarshal makes the old records readable; nothing populates
them, so on an upgraded node the hint is inert until the reserve turns over
on its own, which takes days or never.

Measured on a 2.41M chunk testnet reserve: GetIntoLoc fell back on 100% of
chunks and leveldbstore.Get stayed at 2731 B/chunk, unchanged from the
branch without the hint.

Add migration step 8, which walks the bin index, resolves each address in
the retrieval index once, and writes the location back.

Entries are flushed in windows rather than collected up front: the reserve
holds millions of items and ReserveRepairer's collect-everything approach
would cost hundreds of megabytes while the node is still starting. Since
Location lives in the value and the key stays (Bin, BinID), rewriting does
not disturb the iteration it runs inside. Each window is spread over
NumCPU workers because the per-entry retrieval-index lookup is a random
read and would otherwise leave the disk idle.

The step is idempotent. Entries that already carry a location are skipped,
and entries whose chunk is gone from the chunkstore keep a zero location so
the sampler keeps falling back for them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s5GBDGHX6Nop4RxHhsCr2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant