Skip to content

perf(storer): filter reserve chunks by proximity before unmarshaling in sampler - #5616

Open
gacevicljubisa wants to merge 4 commits into
masterfrom
perf/sample-early-proximity-filter
Open

gacevicljubisa wants to merge 4 commits into
masterfrom
perf/sample-early-proximity-filter

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

This PR introduces an early proximity filter for the reserve chunk iterator during ReserveSample:

  • Direct DB value inspection without unmarshaling: Because ChunkBinItem serializes the chunk address at a fixed offset (byte 9, right after the 1-byte bin and 8-byte bin ID), reserve.ProximityFilter extracts the 32-byte chunk address directly from the raw LevelDB value buffer.
  • Skipping rejected entries at the iterator boundary: If a chunk's proximity to the anchor is < committedDepth, the iterator skips it immediately. This completely avoids allocating a value copy, instantiating &reserve.ChunkBinItem{}, and unmarshaling all fields (Address, BatchID, StampHash, etc.) for non-matching chunks.
  • Where the real benefits apply: On nodes with capacity doubling enabled (committedDepth > storageRadius), the reserve contains chunks in bins $\ge \text{storageRadius}$ that do not satisfy the required committedDepth for the sample round. For these nodes, this eliminates large amounts of heap allocations and unmarshaling work during sampling.

Open API Spec Version Changes (if applicable)

N/A

Motivation and Context (Optional)

Part of reserve sampling performance optimizations.

Related Issue (Optional)

#5174

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

@aloknerurkar aloknerurkar left a comment

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.

Nice!

@acud

acud commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

nice optimization, but: this is also going to silently break without absolutely anyone knowing if the index structure changes in a future migration, breaking the whole sampling process. is there any way to guarantee the contract is maintained?

@aloknerurkar

Copy link
Copy Markdown
Contributor

nice optimization, but: this is also going to silently break without absolutely anyone knowing if the index structure changes in a future migration, breaking the whole sampling process. is there any way to guarantee the contract is maintained?

@gacevicljubisa Maybe we can add a method on chunkBinItem itself to do this instead of defining a separate struct. I agree this might be an issue if we change the chunkBinItem.

Something like UnmarshalAddress which only unmarshals the part we are interested in.

@gacevicljubisa

Copy link
Copy Markdown
Member Author

@acud @aloknerurkar those are valid concerens and I have added 2 new commits trying to address those concerns and proposals.

Instead of having method on chunkBinItem, i have cretead constatnts for each offset to share the layout. Also, I added unit test TestChunkBinItemLayout that pins ChunkBinItem, and any change happens in the future, this test will fail.

Let me know what do you think about this.

@acud

acud commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

consider a slightly simpler and potentially cleaner approach: you don't need to spread the offset into another helper function (nor the consts indices) and can just reuse the existing unmarshal functionality without having to const offsets all over. yes it will do a few more operations but it gives you a bit more operational peace of mind. you need to capture just one ChunkBinItem instance into the filter and you're basically done. i added a small reset function without a receiver because i don't think this should go on the type. small incomplete diff (builds, tests passing):

diff --git a/pkg/storer/internal/reserve/items.go b/pkg/storer/internal/reserve/items.go
index 92603dde..57a720c9 100644
--- a/pkg/storer/internal/reserve/items.go
+++ b/pkg/storer/internal/reserve/items.go
@@ -189,6 +189,15 @@ func (c *ChunkBinItem) Unmarshal(buf []byte) error {
 	return nil
 }
 
+func resetChunkBinItem(c *ChunkBinItem) {
+	c.Bin = 0
+	c.BinID = 0
+	c.Address = swarm.ZeroAddress
+	c.BatchID = nil
+	c.StampHash = nil
+	c.ChunkType = swarm.ChunkTypeUnspecified
+}
+
 // chunkBinItemAddress returns the chunk address bytes of a serialized
 // ChunkBinItem without unmarshaling the rest of it. The returned slice
 // aliases buf.
@@ -202,13 +211,14 @@ func chunkBinItemAddress(buf []byte) ([]byte, bool) {
 // ProximityFilter returns a storage.Filter that excludes serialized ChunkBinItems
 // whose chunk address has a proximity to anchor below committedDepth, without
 // unmarshaling them. Values of unexpected size pass through so Unmarshal reports them.
-func ProximityFilter(anchor []byte, committedDepth uint8) storage.Filter {
+func ProximityFilter(anchor []byte, committedDepth uint8, cbi *ChunkBinItem) storage.Filter {
 	return func(_ string, val []byte) bool {
-		addr, ok := chunkBinItemAddress(val)
-		if !ok {
+		resetChunkBinItem(cbi)
+		err := cbi.Unmarshal(val)
+		if err != nil {
 			return false
 		}
-		return swarm.Proximity(addr, anchor) < committedDepth
+		return swarm.Proximity(cbi.Address.Bytes(), anchor) < committedDepth
 	}
 }
 
diff --git a/pkg/storer/internal/reserve/items_test.go b/pkg/storer/internal/reserve/items_test.go
index 5c304df3..c3cba3ce 100644
--- a/pkg/storer/internal/reserve/items_test.go
+++ b/pkg/storer/internal/reserve/items_test.go
@@ -184,8 +184,8 @@ func TestChunkBinItemAddressAndProximityFilter(t *testing.T) {
 	if !ok || !swarm.NewAddress(addr).Equal(closeAddr) {
 		t.Fatalf("expected address %s, got %s", closeAddr, swarm.NewAddress(addr))
 	}
-
-	filter := reserve.ProximityFilter(baseAddr.Bytes(), 1)
+	var cbi reserve.ChunkBinItem
+	filter := reserve.ProximityFilter(baseAddr.Bytes(), 1, &cbi)
 	// farAddr has proximity 0 to baseAddr (< 1) -> should be filtered out (true)
 	if !filter("", bufFar) {
 		t.Fatal("expected farAddr to be filtered out")
@@ -229,9 +229,10 @@ func TestProximityFilterMatchesUnmarshal(t *testing.T) {
 			t.Fatal(err)
 		}
 
+		var cbi reserve.ChunkBinItem
 		for depth := uint8(0); depth <= swarm.MaxPO+1; depth++ {
 			want := swarm.Proximity(unmarshaled.Address.Bytes(), anchor.Bytes()) < depth
-			got := reserve.ProximityFilter(anchor.Bytes(), depth)("", buf)
+			got := reserve.ProximityFilter(anchor.Bytes(), depth, &cbi)("", buf)
 			if got != want {
 				t.Fatalf("po %d depth %d: filter excludes=%v, unmarshaled item excludes=%v", po, depth, got, want)
 			}
diff --git a/pkg/storer/sample.go b/pkg/storer/sample.go
index f18cda08..08a13f6d 100644
--- a/pkg/storer/sample.go
+++ b/pkg/storer/sample.go
@@ -102,8 +102,8 @@ func (db *DB) ReserveSample(
 			close(chunkC)
 			addStats(stats)
 		}()
-
-		filter := reserve.ProximityFilter(anchor, committedDepth)
+		var cbi reserve.ChunkBinItem
+		filter := reserve.ProximityFilter(anchor, committedDepth, &cbi)
 
 		err := db.reserve.IterateChunksItems(db.StorageRadius(), func(ch *reserve.ChunkBinItem) (bool, error) {
 			select {

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.

4 participants