[#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
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)
}