Skip to content

perf(storer): reuse buffers and eliminate per-chunk allocations in reserve sample - #5612

Open
gacevicljubisa wants to merge 7 commits into
masterfrom
perf/sample-01-hoist-chunkstore
Open

gacevicljubisa wants to merge 7 commits into
masterfrom
perf/sample-01-hoist-chunkstore

Conversation

@gacevicljubisa

@gacevicljubisa gacevicljubisa commented Sep 14, 2026

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

This PR addresses per-chunk allocations and buffer overhead in ReserveSample (issue #5174):

  1. GetterInto interface: Adds storage.GetterInto to allow reading chunk data directly into a caller-provided reusable buffer, avoiding per-call buffer allocations in storage readers.
  2. Hoist ChunkStore handle: Instantiates ChunkStore() once per worker instead of per chunk, eliminating 3 allocations per chunk.
  3. Per-worker reusable buffer: Each sampler worker reuses a swarm.SocMaxChunkSize buffer for chunk reading and feeds raw byte slices directly into transformedAddress, eliminating swarm.NewChunk heap
    escapes.
  4. Lazy chunk loading for candidates: Full chunk payloads are no longer pushed through sampleItemChan for all millions of chunks. Full chunk data is loaded in Phase 3 only for the ~200 items that
    actually qualify for the sample candidate set.
  5. Raw batch ID transfer: Avoids instantiating postage.NewStamp for every iterated chunk in workers by passing raw batchID through an internal candidate struct.
  6. Safety guards & benchmarks: Adds assertSampleDataIntact and assertSampleDataNotShared to prevent buffer aliasing/corruption regressions, adds BenchmarkChunkStoreGet micro-benchmark, and adds
    BenchmarkReserveSample10k.

A/B on bee-light-testnet, two nodes with identical config (no SIMD, verbosity 3, 1500m CPU, storage radius 2, isWarmingUp: false). Sample triggered via GET /rchash/2/<own-overlay>/<anchor2>; allocations attributed with pprof -diff_base focused on the ReserveSample subtree.

Per chunk (ReserveSample subtree) master b6537ea2 this branch Δ
Allocations 132.7 104.0 −21.7%
↳ chunk retrieval 60.5 36.4 −39.9%
Bytes 11,007 B 4,489 B −59.2%
↳ chunk retrieval 8,774 B 2,615 B −70.2%
chunkstore.readChunk buffer 4,876 B 0 B −100%
transformedAddress (control) 68.3 67.6 −1.1%
bmt.Write (control) 63.0 62.6 −0.7%

Total allocation churn per sample round: 24.65 GB → 10.30 GB over 2.405M chunks.

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 2 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
@gacevicljubisa
gacevicljubisa force-pushed the perf/sample-01-hoist-chunkstore branch from 444d89d to 841bbaf Compare September 14, 2026 14:11
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
Comment thread pkg/file/joiner/joiner_test.go
Comment thread pkg/storage/chunkstore.go

type ChunkStore interface {
Getter
GetterInto

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need to add it to the ChunkStore interface. Since we only use ReadOnlyChunkStore for sampling, we can only add it to the interface below. I think we can even avoid adding it to transaction.ChunkStore also.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping it makes ReadOnlyChunkStore no longer a subset of ChunkStore, which breaks the test storages and the mock storer that return the same value as both. Then even more changes are needed...

return ch, err
}

func (c *chunkStoreTrx) GetInto(ctx context.Context, addr swarm.Address, buf []byte) (n int, err error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check comment above. Since we only need ReadOnlyChunkStore to provide this function, we can avoid adding it here.

@gacevicljubisa
gacevicljubisa requested a review from acud September 15, 2026 21:00
Comment thread pkg/storer/sample.go
continue
}

ch, err := phase3ChunkStore.Get(ctx, item.chunkAddress)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc there's another thing to optimize here: notice that there could be many inserts into the sample, however, the sample is fixed size and, finally it converges onto a certain set of chunks. what this means is that you're inserting an item into the sample without knowing whether it would actually survive into the final sample set. so the next plausible thing to do here is to not Get the chunk here - insert the sample item without the chunk data (but leave all the other fields on the type), and backfill the surviving sample members once the sample is finalized i.e. before returning the sample at L318

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to even do the stamp loading at the same step. in fact at this stage you need not save anything into the sample apart from the address and the transformed address

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have skiped it intentionally, on 4M chunks, it is expected 200-250 chunks to land in phase 3. This is case on testnet and mainnet. Deferring validation could leave the samle with less then 16 items. I would keep this PR as is, and maybe we can have a follow up. Is that fine by you?

@acud acud Sep 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok 👍 btw after calin's last PR i am no longer sure about my suggestion to rely on cap instead of len. having seen how the std lib does reads - it indeed usually uses len, not cap. so - my bad

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done with additional test.

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.

3 participants