-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbench_test.go
More file actions
115 lines (101 loc) · 3.35 KB
/
Copy pathbench_test.go
File metadata and controls
115 lines (101 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package lockd
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/imharjot/lockd/clock"
)
// simulatedJournalCost stands in for the durable write the engine performs
// while it owns a key (a WAL append, a few microseconds of syscall). The
// actor holds no shared lock across this work; the naive locker holds its one
// global mutex across it, which is the whole point of the comparison.
const simulatedJournalCost = 3 * time.Microsecond
// naiveLocker is the strawman the actor model is compared against: a single
// global mutex guarding a map of held keys. Every acquire/release on any key
// serializes on the one lock, and the durable write happens while the lock is
// held, so unrelated keys queue behind each other's I/O.
type naiveLocker struct {
mu sync.Mutex
held map[string]string // key -> holder
token atomic.Uint64
}
func newNaiveLocker() *naiveLocker {
return &naiveLocker{held: make(map[string]string)}
}
func (n *naiveLocker) acquire(key, holder string) (uint64, bool) {
n.mu.Lock()
defer n.mu.Unlock()
if _, ok := n.held[key]; ok {
return 0, false
}
n.held[key] = holder
tok := n.token.Add(1)
busyWait(simulatedJournalCost) // durable write under the global lock
return tok, true
}
func (n *naiveLocker) release(key, holder string) {
n.mu.Lock()
defer n.mu.Unlock()
if n.held[key] == holder {
delete(n.held, key)
busyWait(simulatedJournalCost)
}
}
// busyWait spins for d. Used to model fixed per-op durable-write cost without
// depending on the OS scheduler waking a sleeper.
func busyWait(d time.Duration) {
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
}
}
// slowJournal models the same per-write durable cost the naive locker pays,
// so both benchmarks do equal work; only the concurrency strategy differs.
type slowJournal struct{}
func (slowJournal) Append(Record) error {
busyWait(simulatedJournalCost)
return nil
}
func (slowJournal) Close() error { return nil }
// BenchmarkActorAcquireRelease measures the sharded actor engine under
// parallel acquire/release across many distinct keys. Each grant/release pays
// the same simulated durable-write cost as the naive locker, but the write
// runs on a per-shard actor rather than under one global mutex.
func BenchmarkActorAcquireRelease(b *testing.B) {
e := New(Config{Shards: 32, Clock: clock.NewReal(), Tick: 50 * time.Millisecond, WheelSlots: 512, WAL: slowJournal{}})
defer e.Close()
ctx := context.Background()
var ctr atomic.Uint64
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
key := keyName(ctr.Add(1))
g, err := e.Acquire(ctx, key, "h", time.Minute)
if err == nil {
_ = e.Release(key, "h", g.Token)
}
}
})
}
// BenchmarkNaiveMutexAcquireRelease runs the same workload against the single
// global mutex. The gap between this and the actor benchmark is the cost of
// funnelling unrelated keys through one lock.
func BenchmarkNaiveMutexAcquireRelease(b *testing.B) {
n := newNaiveLocker()
var ctr atomic.Uint64
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
key := keyName(ctr.Add(1))
if _, ok := n.acquire(key, "h"); ok {
n.release(key, "h")
}
}
})
}
func keyName(i uint64) string {
const set = "abcdefghijklmnop"
// spread across ~4096 keys so shards and the map both see many keys
return string([]byte{set[i&0xf], set[(i>>4)&0xf], set[(i>>8)&0xf]})
}