From 81f925d5a05e7ad58433b089b33de843693757c8 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 14 Jun 2022 19:34:23 +0300 Subject: [PATCH] [#1516] metabase: Optimize `ListWithCursor` for long listings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache buckets outside of the main loop and allocate memory for the resulting offset only once. ``` name old time/op new time/op delta ListWithCursor/1_item-8 6.45µs ±14% 5.79µs ±11% -10.24% (p=0.002 n=10+10) ListWithCursor/10_items-8 20.9µs ±17% 17.3µs ± 9% -17.27% (p=0.000 n=10+10) ListWithCursor/100_items-8 153µs ±12% 131µs ± 9% -14.63% (p=0.000 n=10+10) name old alloc/op new alloc/op delta ListWithCursor/1_item-8 2.31kB ± 0% 1.91kB ± 0% -17.46% (p=0.000 n=10+10) ListWithCursor/10_items-8 6.94kB ± 0% 5.50kB ± 0% -20.78% (p=0.000 n=8+8) ListWithCursor/100_items-8 53.3kB ± 0% 41.5kB ± 0% -22.18% (p=0.000 n=10+10) name old allocs/op new allocs/op delta ListWithCursor/1_item-8 40.0 ± 0% 34.0 ± 0% -15.00% (p=0.000 n=10+10) ListWithCursor/10_items-8 121 ± 0% 100 ± 0% -17.36% (p=0.000 n=10+10) ListWithCursor/100_items-8 930 ± 0% 758 ± 0% -18.49% (p=0.000 n=10+10) ``` Signed-off-by: Evgenii Stratonikov --- pkg/local_object_storage/metabase/list.go | 71 ++++++++++++----------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/pkg/local_object_storage/metabase/list.go b/pkg/local_object_storage/metabase/list.go index a690f7a86..1140162fd 100644 --- a/pkg/local_object_storage/metabase/list.go +++ b/pkg/local_object_storage/metabase/list.go @@ -1,8 +1,8 @@ package meta import ( + "bytes" "errors" - "strings" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -102,11 +102,17 @@ func (db *DB) listWithCursor(tx *bbolt.Tx, result []oid.Address, count int, curs } var containerID cid.ID + var offset []byte + graveyardBkt := tx.Bucket(graveyardBucketName) + garbageBkt := tx.Bucket(garbageBucketName) + + const idSize = 44 // size of the stringified object and container ids + var rawAddr = make([]byte, idSize*2+1) loop: for ; name != nil; name, _ = c.Next() { - postfix, ok := parseContainerIDWithPostfix(&containerID, name) - if !ok { + b58CID, postfix := parseContainerIDWithPostfix(&containerID, name) + if b58CID == nil { continue } @@ -120,8 +126,13 @@ loop: continue } - prefix := containerID.EncodeToString() + "/" - result, cursor = selectNFromBucket(tx, name, prefix, containerID, result, count, cursor, threshold) + bkt := tx.Bucket(name) + if bkt != nil { + rawAddr = append(rawAddr[:0], b58CID...) + rawAddr = append(rawAddr, '/') + result, offset, cursor = selectNFromBucket(bkt, graveyardBkt, garbageBkt, rawAddr, containerID, + result, count, cursor, threshold) + } bucketName = name if len(result) >= count { break loop @@ -132,6 +143,13 @@ loop: threshold = true } + if offset != nil { + // new slice is much faster but less memory efficient + // we need to copy, because offset exists during bbolt tx + cursor.inBucketOffset = make([]byte, len(offset)) + copy(cursor.inBucketOffset, offset) + } + if len(result) == 0 { return nil, nil, ErrEndOfListing } @@ -146,20 +164,15 @@ loop: // selectNFromBucket similar to selectAllFromBucket but uses cursor to find // object to start selecting from. Ignores inhumed objects. -func selectNFromBucket(tx *bbolt.Tx, - name []byte, // bucket name - prefix string, // container ID prefix, optimization +func selectNFromBucket(bkt *bbolt.Bucket, // main bucket + graveyardBkt, garbageBkt *bbolt.Bucket, // cached graveyard buckets + addrRaw []byte, // container ID prefix, optimization cnt cid.ID, // container ID to []oid.Address, // listing result limit int, // stop listing at `limit` items in result cursor *Cursor, // start from cursor object threshold bool, // ignore cursor and start immediately -) ([]oid.Address, *Cursor) { - bkt := tx.Bucket(name) - if bkt == nil { - return to, cursor - } - +) ([]oid.Address, []byte, *Cursor) { if cursor == nil { cursor = new(Cursor) } @@ -175,12 +188,6 @@ func selectNFromBucket(tx *bbolt.Tx, k, _ = c.Next() // we are looking for objects _after_ the cursor } - addrRaw := make([]byte, len(prefix)+44) - copy(addrRaw, prefix) - - graveyardBkt := tx.Bucket(graveyardBucketName) - garbageBkt := tx.Bucket(garbageBucketName) - for ; k != nil; k, _ = c.Next() { if count >= limit { break @@ -192,8 +199,7 @@ func selectNFromBucket(tx *bbolt.Tx, } offset = k - addrRaw = append(addrRaw[:len(prefix)], k...) - if inGraveyardWithKey(addrRaw, graveyardBkt, garbageBkt) > 0 { + if inGraveyardWithKey(append(addrRaw, k...), graveyardBkt, garbageBkt) > 0 { continue } @@ -204,29 +210,24 @@ func selectNFromBucket(tx *bbolt.Tx, count++ } - // new slice is much faster but less memory efficient - // we need to copy, because offset exists during bbolt tx - cursor.inBucketOffset = make([]byte, len(offset)) - copy(cursor.inBucketOffset, offset) - - return to, cursor + return to, offset, cursor } -func parseContainerIDWithPostfix(containerID *cid.ID, name []byte) (string, bool) { +func parseContainerIDWithPostfix(containerID *cid.ID, name []byte) ([]byte, string) { var ( - containerIDStr = string(name) - postfix string + containerIDStr = name + postfix []byte ) - ind := strings.Index(string(name), invalidBase58String) + ind := bytes.IndexByte(name, invalidBase58String[0]) if ind > 0 { postfix = containerIDStr[ind:] containerIDStr = containerIDStr[:ind] } - if err := containerID.DecodeString(containerIDStr); err != nil { - return "", false + if err := containerID.DecodeString(string(containerIDStr)); err != nil { + return nil, "" } - return postfix, true + return containerIDStr, string(postfix) }