From eb9df85b989f861f50891fc21732ce56e56dabc3 Mon Sep 17 00:00:00 2001
From: Evgenii Stratonikov <e.stratonikov@yadro.com>
Date: Thu, 20 Mar 2025 10:23:26 +0300
Subject: [PATCH] [#1685] metabase: Cache primary bucket
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

```
goos: linux
goarch: amd64
pkg: git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
                          │   expired    │               primary               │
                          │    sec/op    │    sec/op     vs base               │
Select/string_equal-8       3.529m ± 11%   3.689m ±  7%  +4.55% (p=0.023 n=10)
Select/string_not_equal-8   3.440m ±  7%   3.543m ± 13%       ~ (p=0.190 n=10)
Select/common_prefix-8      3.240m ±  6%   3.050m ±  5%  -5.85% (p=0.005 n=10)
Select/unknown-8            3.198m ±  6%   2.928m ±  8%  -8.44% (p=0.003 n=10)
geomean                     3.349m         3.287m        -1.84%

                          │   expired    │               primary               │
                          │     B/op     │     B/op      vs base               │
Select/string_equal-8       1.885Mi ± 0%   1.786Mi ± 0%  -5.23% (p=0.000 n=10)
Select/string_not_equal-8   1.885Mi ± 0%   1.786Mi ± 0%  -5.23% (p=0.000 n=10)
Select/common_prefix-8      1.885Mi ± 0%   1.786Mi ± 0%  -5.23% (p=0.000 n=10)
Select/unknown-8            1.877Mi ± 0%   1.779Mi ± 0%  -5.26% (p=0.000 n=10)
geomean                     1.883Mi        1.784Mi       -5.24%

                          │   expired   │              primary               │
                          │  allocs/op  │  allocs/op   vs base               │
Select/string_equal-8       46.04k ± 0%   43.04k ± 0%  -6.50% (p=0.000 n=10)
Select/string_not_equal-8   46.04k ± 0%   43.04k ± 0%  -6.50% (p=0.000 n=10)
Select/common_prefix-8      46.04k ± 0%   43.04k ± 0%  -6.50% (p=0.000 n=10)
Select/unknown-8            45.05k ± 0%   42.05k ± 0%  -6.65% (p=0.000 n=10)
geomean                     45.79k        42.79k       -6.54%
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
---
 pkg/local_object_storage/metabase/bucket_cache.go | 15 ++++++++++++++-
 pkg/local_object_storage/metabase/get.go          | 15 ++++++++++-----
 pkg/local_object_storage/metabase/select.go       | 12 ++++++------
 3 files changed, 30 insertions(+), 12 deletions(-)

diff --git a/pkg/local_object_storage/metabase/bucket_cache.go b/pkg/local_object_storage/metabase/bucket_cache.go
index 14c164afc..69553d55c 100644
--- a/pkg/local_object_storage/metabase/bucket_cache.go
+++ b/pkg/local_object_storage/metabase/bucket_cache.go
@@ -10,10 +10,14 @@ type bucketCache struct {
 	graveyard *bbolt.Bucket
 	garbage   *bbolt.Bucket
 	expired   map[cid.ID]*bbolt.Bucket
+	primary   map[cid.ID]*bbolt.Bucket
 }
 
 func newBucketCache() *bucketCache {
-	return &bucketCache{expired: make(map[cid.ID]*bbolt.Bucket)}
+	return &bucketCache{
+		expired: make(map[cid.ID]*bbolt.Bucket),
+		primary: make(map[cid.ID]*bbolt.Bucket),
+	}
 }
 
 func getLockedBucket(bc *bucketCache, tx *bbolt.Tx) *bbolt.Bucket {
@@ -55,6 +59,15 @@ func getExpiredBucket(bc *bucketCache, tx *bbolt.Tx, cnr cid.ID) *bbolt.Bucket {
 	return getMappedBucket(bc.expired, tx, objectToExpirationEpochBucketName, cnr)
 }
 
+func getPrimaryBucket(bc *bucketCache, tx *bbolt.Tx, cnr cid.ID) *bbolt.Bucket {
+	if bc == nil {
+		bucketName := make([]byte, bucketKeySize)
+		bucketName = primaryBucketName(cnr, bucketName)
+		return tx.Bucket(bucketName)
+	}
+	return getMappedBucket(bc.primary, tx, primaryBucketName, cnr)
+}
+
 func getMappedBucket(m map[cid.ID]*bbolt.Bucket, tx *bbolt.Tx, nameFunc func(cid.ID, []byte) []byte, cnr cid.ID) *bbolt.Bucket {
 	value, ok := m[cnr]
 	if ok {
diff --git a/pkg/local_object_storage/metabase/get.go b/pkg/local_object_storage/metabase/get.go
index 615add1af..821810c09 100644
--- a/pkg/local_object_storage/metabase/get.go
+++ b/pkg/local_object_storage/metabase/get.go
@@ -88,8 +88,12 @@ func (db *DB) Get(ctx context.Context, prm GetPrm) (res GetRes, err error) {
 }
 
 func (db *DB) get(tx *bbolt.Tx, addr oid.Address, key []byte, checkStatus, raw bool, currEpoch uint64) (*objectSDK.Object, error) {
+	return db.getWithCache(nil, tx, addr, key, checkStatus, raw, currEpoch)
+}
+
+func (db *DB) getWithCache(bc *bucketCache, tx *bbolt.Tx, addr oid.Address, key []byte, checkStatus, raw bool, currEpoch uint64) (*objectSDK.Object, error) {
 	if checkStatus {
-		st, err := objectStatus(tx, addr, currEpoch)
+		st, err := objectStatusWithCache(bc, tx, addr, currEpoch)
 		if err != nil {
 			return nil, err
 		}
@@ -109,12 +113,13 @@ func (db *DB) get(tx *bbolt.Tx, addr oid.Address, key []byte, checkStatus, raw b
 	bucketName := make([]byte, bucketKeySize)
 
 	// check in primary index
-	data := getFromBucket(tx, primaryBucketName(cnr, bucketName), key)
-	if len(data) != 0 {
-		return obj, obj.Unmarshal(data)
+	if b := getPrimaryBucket(bc, tx, cnr); b != nil {
+		if data := b.Get(key); len(data) != 0 {
+			return obj, obj.Unmarshal(data)
+		}
 	}
 
-	data = getFromBucket(tx, ecInfoBucketName(cnr, bucketName), key)
+	data := getFromBucket(tx, ecInfoBucketName(cnr, bucketName), key)
 	if len(data) != 0 {
 		return nil, getECInfoError(tx, cnr, data)
 	}
diff --git a/pkg/local_object_storage/metabase/select.go b/pkg/local_object_storage/metabase/select.go
index a95384753..60da50671 100644
--- a/pkg/local_object_storage/metabase/select.go
+++ b/pkg/local_object_storage/metabase/select.go
@@ -154,7 +154,7 @@ func (db *DB) selectObjects(tx *bbolt.Tx, cnr cid.ID, fs objectSDK.SearchFilters
 			continue // ignore removed objects
 		}
 
-		addr, match := db.matchSlowFilters(tx, addr, group.slowFilters, currEpoch)
+		addr, match := db.matchSlowFilters(bc, tx, addr, group.slowFilters, currEpoch)
 		if !match {
 			continue // ignore objects with unmatched slow filters
 		}
@@ -452,13 +452,13 @@ func (db *DB) selectObjectID(
 }
 
 // matchSlowFilters return true if object header is matched by all slow filters.
-func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr oid.Address, f objectSDK.SearchFilters, currEpoch uint64) (oid.Address, bool) {
+func (db *DB) matchSlowFilters(bc *bucketCache, tx *bbolt.Tx, addr oid.Address, f objectSDK.SearchFilters, currEpoch uint64) (oid.Address, bool) {
 	result := addr
 	if len(f) == 0 {
 		return result, true
 	}
 
-	obj, isECChunk, err := db.getObjectForSlowFilters(tx, addr, currEpoch)
+	obj, isECChunk, err := db.getObjectForSlowFilters(bc, tx, addr, currEpoch)
 	if err != nil {
 		return result, false
 	}
@@ -516,9 +516,9 @@ func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr oid.Address, f objectSDK.Searc
 	return result, true
 }
 
-func (db *DB) getObjectForSlowFilters(tx *bbolt.Tx, addr oid.Address, currEpoch uint64) (*objectSDK.Object, bool, error) {
+func (db *DB) getObjectForSlowFilters(bc *bucketCache, tx *bbolt.Tx, addr oid.Address, currEpoch uint64) (*objectSDK.Object, bool, error) {
 	buf := make([]byte, addressKeySize)
-	obj, err := db.get(tx, addr, buf, false, false, currEpoch)
+	obj, err := db.getWithCache(bc, tx, addr, buf, false, false, currEpoch)
 	if err != nil {
 		var ecInfoError *objectSDK.ECInfoError
 		if errors.As(err, &ecInfoError) {
@@ -528,7 +528,7 @@ func (db *DB) getObjectForSlowFilters(tx *bbolt.Tx, addr oid.Address, currEpoch
 					continue
 				}
 				addr.SetObject(objID)
-				obj, err = db.get(tx, addr, buf, true, false, currEpoch)
+				obj, err = db.getWithCache(bc, tx, addr, buf, true, false, currEpoch)
 				if err == nil {
 					return obj, true, nil
 				}