[#1516] metabase: Optimize ListWithCursor for long listings

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 <evgeniy@nspcc.ru>
This commit is contained in:
Evgenii Stratonikov 2022-06-14 19:34:23 +03:00 committed by LeL
parent a93373fe71
commit 81f925d5a0

View file

@ -1,8 +1,8 @@
package meta package meta
import ( import (
"bytes"
"errors" "errors"
"strings"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/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 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: loop:
for ; name != nil; name, _ = c.Next() { for ; name != nil; name, _ = c.Next() {
postfix, ok := parseContainerIDWithPostfix(&containerID, name) b58CID, postfix := parseContainerIDWithPostfix(&containerID, name)
if !ok { if b58CID == nil {
continue continue
} }
@ -120,8 +126,13 @@ loop:
continue continue
} }
prefix := containerID.EncodeToString() + "/" bkt := tx.Bucket(name)
result, cursor = selectNFromBucket(tx, name, prefix, containerID, result, count, cursor, threshold) 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 bucketName = name
if len(result) >= count { if len(result) >= count {
break loop break loop
@ -132,6 +143,13 @@ loop:
threshold = true 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 { if len(result) == 0 {
return nil, nil, ErrEndOfListing return nil, nil, ErrEndOfListing
} }
@ -146,20 +164,15 @@ loop:
// selectNFromBucket similar to selectAllFromBucket but uses cursor to find // selectNFromBucket similar to selectAllFromBucket but uses cursor to find
// object to start selecting from. Ignores inhumed objects. // object to start selecting from. Ignores inhumed objects.
func selectNFromBucket(tx *bbolt.Tx, func selectNFromBucket(bkt *bbolt.Bucket, // main bucket
name []byte, // bucket name graveyardBkt, garbageBkt *bbolt.Bucket, // cached graveyard buckets
prefix string, // container ID prefix, optimization addrRaw []byte, // container ID prefix, optimization
cnt cid.ID, // container ID cnt cid.ID, // container ID
to []oid.Address, // listing result to []oid.Address, // listing result
limit int, // stop listing at `limit` items in result limit int, // stop listing at `limit` items in result
cursor *Cursor, // start from cursor object cursor *Cursor, // start from cursor object
threshold bool, // ignore cursor and start immediately threshold bool, // ignore cursor and start immediately
) ([]oid.Address, *Cursor) { ) ([]oid.Address, []byte, *Cursor) {
bkt := tx.Bucket(name)
if bkt == nil {
return to, cursor
}
if cursor == nil { if cursor == nil {
cursor = new(Cursor) cursor = new(Cursor)
} }
@ -175,12 +188,6 @@ func selectNFromBucket(tx *bbolt.Tx,
k, _ = c.Next() // we are looking for objects _after_ the cursor 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() { for ; k != nil; k, _ = c.Next() {
if count >= limit { if count >= limit {
break break
@ -192,8 +199,7 @@ func selectNFromBucket(tx *bbolt.Tx,
} }
offset = k offset = k
addrRaw = append(addrRaw[:len(prefix)], k...) if inGraveyardWithKey(append(addrRaw, k...), graveyardBkt, garbageBkt) > 0 {
if inGraveyardWithKey(addrRaw, graveyardBkt, garbageBkt) > 0 {
continue continue
} }
@ -204,29 +210,24 @@ func selectNFromBucket(tx *bbolt.Tx,
count++ count++
} }
// new slice is much faster but less memory efficient return to, offset, cursor
// we need to copy, because offset exists during bbolt tx
cursor.inBucketOffset = make([]byte, len(offset))
copy(cursor.inBucketOffset, offset)
return to, cursor
} }
func parseContainerIDWithPostfix(containerID *cid.ID, name []byte) (string, bool) { func parseContainerIDWithPostfix(containerID *cid.ID, name []byte) ([]byte, string) {
var ( var (
containerIDStr = string(name) containerIDStr = name
postfix string postfix []byte
) )
ind := strings.Index(string(name), invalidBase58String) ind := bytes.IndexByte(name, invalidBase58String[0])
if ind > 0 { if ind > 0 {
postfix = containerIDStr[ind:] postfix = containerIDStr[ind:]
containerIDStr = containerIDStr[:ind] containerIDStr = containerIDStr[:ind]
} }
if err := containerID.DecodeString(containerIDStr); err != nil { if err := containerID.DecodeString(string(containerIDStr)); err != nil {
return "", false return nil, ""
} }
return postfix, true return containerIDStr, string(postfix)
} }