frostfs-node/pkg/local_object_storage/pilorama/batch.go
Evgenii Stratonikov 15102e6dfd [#1471] Replace sort.Slice in some places
`slices.SortFunc` doesn't use reflection and is a bit faster.
I have done some micro-benchmarks for `[]NodeInfo`:
```
$ benchstat -col "/func" out
goos: linux
goarch: amd64
pkg: git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/pilorama
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
       │ sort.Slice  │           slices.SortFunc           │
       │   sec/op    │   sec/op     vs base                │
Sort-8   2.130µ ± 2%   1.253µ ± 2%  -41.20% (p=0.000 n=10)
```

Haven't included them, though, as they I don't see them being used a
lot.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2024-11-06 08:07:32 +00:00

118 lines
3.2 KiB
Go

package pilorama
import (
"cmp"
"encoding/binary"
"slices"
"sync"
"time"
cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"go.etcd.io/bbolt"
)
type batch struct {
forest *boltForest
timer *time.Timer
// mtx protects timer and operations fields.
// Because mtx can be taken inside a transaction,
// transactions MUST NOT be executed with the mutex taken to avoid a deadlock.
mtx sync.Mutex
start sync.Once
cid cidSDK.ID
treeID string
results []chan<- error
operations []*Move
}
func (b *batch) trigger() {
b.mtx.Lock()
if b.timer != nil {
b.timer.Stop()
}
b.mtx.Unlock()
b.start.Do(b.run)
}
func (b *batch) run() {
fullID := bucketName(b.cid, b.treeID)
err := b.forest.db.Update(func(tx *bbolt.Tx) error {
bLog, bTree, err := b.forest.getTreeBuckets(tx, fullID)
if err != nil {
return err
}
b.mtx.Lock()
b.timer = nil
b.mtx.Unlock()
// Sorting without a mutex is ok, because we append to this slice only if timer is non-nil.
// See (*boltForest).addBatch for details.
slices.SortFunc(b.operations, func(mi, mj *Move) int {
return cmp.Compare(mi.Time, mj.Time)
})
b.operations = slices.CompactFunc(b.operations, func(x, y *Move) bool { return x.Time == y.Time })
// Our main use-case is addition of new items. In this case,
// we do not need to perform undo()/redo(), just do().
// https://github.com/trvedata/move-op/blob/6c23447c12a7862ff31b7fc2205f6c90fbdb9dc0/proof/Move_Create.thy#L259
//
// For this optimization to work we need to ensure three things:
// 1. The node itself is not yet in tree.
// 2. The node is not a parent. This case is not mentioned in the article, because
// they consider a "static order" (perform all CREATE operations before MOVE).
// We need this because if node _is_ a parent, we could violate (3) for some late operation.
// See TestForest_ApplySameOperation for details.
// 3. Parent of each operation is already in tree.
var parents map[uint64]struct{}
var cKey [maxKeySize]byte
var slow bool
for i := range b.operations {
_, _, _, inTree := b.forest.getState(bTree, stateKey(cKey[:], b.operations[i].Child))
if inTree {
slow = true
break
}
key := childrenKey(cKey[:], b.operations[i].Child, 0)
k, _ := bTree.Cursor().Seek(key)
if len(k) == childrenKeySize && binary.LittleEndian.Uint64(k[1:]) == b.operations[i].Child {
slow = true
break
}
if b.operations[i].Parent == RootID {
continue
} else if parents == nil {
// Attaching key only to root is done frequently,
// no allocations are performed unless necessary.
parents = make(map[uint64]struct{})
} else if _, ok := parents[b.operations[i].Parent]; ok {
continue
}
p := b.operations[i].Parent
_, ts, _, inTree := b.forest.getState(bTree, stateKey(cKey[:], p))
if !inTree || b.operations[0].Time < ts {
slow = true
break
}
parents[b.operations[i].Parent] = struct{}{}
}
if slow {
var lm Move
return b.forest.applyOperation(bLog, bTree, b.operations, &lm)
}
for i := range b.operations {
if err := b.forest.do(bLog, bTree, cKey[:], b.operations[i]); err != nil {
return err
}
}
return nil
})
for i := range b.results {
b.results[i] <- err
}
}