-
Notifications
You must be signed in to change notification settings - Fork 85
/
fastclock.go
129 lines (106 loc) · 3.5 KB
/
fastclock.go
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package regexp2
import (
"sync"
"sync/atomic"
"time"
)
// fasttime holds a time value (ticks since clock initialization)
type fasttime int64
// fastclock provides a fast clock implementation.
//
// A background goroutine periodically stores the current time
// into an atomic variable.
//
// A deadline can be quickly checked for expiration by comparing
// its value to the clock stored in the atomic variable.
//
// The goroutine automatically stops once clockEnd is reached.
// (clockEnd covers the largest deadline seen so far + some
// extra time). This ensures that if regexp2 with timeouts
// stops being used we will stop background work.
type fastclock struct {
// instances of atomicTime must be at the start of the struct (or at least 64-bit aligned)
// otherwise 32-bit architectures will panic
current atomicTime // Current time (approximate)
clockEnd atomicTime // When clock updater is supposed to stop (>= any existing deadline)
// current and clockEnd can be read via atomic loads.
// Reads and writes of other fields require mu to be held.
mu sync.Mutex
start time.Time // Time corresponding to fasttime(0)
running bool // Is a clock updater running?
}
var fast fastclock
// reached returns true if current time is at or past t.
func (t fasttime) reached() bool {
return fast.current.read() >= t
}
// makeDeadline returns a time that is approximately time.Now().Add(d)
func makeDeadline(d time.Duration) fasttime {
// Increase the deadline since the clock we are reading may be
// just about to tick forwards.
end := fast.current.read() + durationToTicks(d+clockPeriod)
// Start or extend clock if necessary.
if end > fast.clockEnd.read() {
extendClock(end)
}
return end
}
// extendClock ensures that clock is live and will run until at least end.
func extendClock(end fasttime) {
fast.mu.Lock()
defer fast.mu.Unlock()
if fast.start.IsZero() {
fast.start = time.Now()
}
// Extend the running time to cover end as well as a bit of slop.
if shutdown := end + durationToTicks(time.Second); shutdown > fast.clockEnd.read() {
fast.clockEnd.write(shutdown)
}
// Start clock if necessary
if !fast.running {
fast.running = true
go runClock()
}
}
// stop the timeout clock in the background
// should only used for unit tests to abandon the background goroutine
func stopClock() {
fast.mu.Lock()
if fast.running {
fast.clockEnd.write(fasttime(0))
}
fast.mu.Unlock()
// pause until not running
// get and release the lock
isRunning := true
for isRunning {
time.Sleep(clockPeriod / 2)
fast.mu.Lock()
isRunning = fast.running
fast.mu.Unlock()
}
}
func durationToTicks(d time.Duration) fasttime {
// Downscale nanoseconds to approximately a millisecond so that we can avoid
// overflow even if the caller passes in math.MaxInt64.
return fasttime(d) >> 20
}
const DefaultClockPeriod = 100 * time.Millisecond
// clockPeriod is the approximate interval between updates of approximateClock.
var clockPeriod = DefaultClockPeriod
func runClock() {
fast.mu.Lock()
defer fast.mu.Unlock()
for fast.current.read() <= fast.clockEnd.read() {
// Unlock while sleeping.
fast.mu.Unlock()
time.Sleep(clockPeriod)
fast.mu.Lock()
newTime := durationToTicks(time.Since(fast.start))
fast.current.write(newTime)
}
fast.running = false
}
type atomicTime struct{ v int64 } // Should change to atomic.Int64 when we can use go 1.19
func (t *atomicTime) read() fasttime { return fasttime(atomic.LoadInt64(&t.v)) }
func (t *atomicTime) write(v fasttime) { atomic.StoreInt64(&t.v, int64(v)) }