1package regexp2
2
3import (
4 "sync"
5 "sync/atomic"
6 "time"
7)
8
9// fasttime holds a time value (ticks since clock initialization)
10type fasttime int64
11
12// fastclock provides a fast clock implementation.
13//
14// A background goroutine periodically stores the current time
15// into an atomic variable.
16//
17// A deadline can be quickly checked for expiration by comparing
18// its value to the clock stored in the atomic variable.
19//
20// The goroutine automatically stops once clockEnd is reached.
21// (clockEnd covers the largest deadline seen so far + some
22// extra time). This ensures that if regexp2 with timeouts
23// stops being used we will stop background work.
24type fastclock struct {
25 // instances of atomicTime must be at the start of the struct (or at least 64-bit aligned)
26 // otherwise 32-bit architectures will panic
27
28 current atomicTime // Current time (approximate)
29 clockEnd atomicTime // When clock updater is supposed to stop (>= any existing deadline)
30
31 // current and clockEnd can be read via atomic loads.
32 // Reads and writes of other fields require mu to be held.
33 mu sync.Mutex
34 start time.Time // Time corresponding to fasttime(0)
35 running bool // Is a clock updater running?
36}
37
38var fast fastclock
39
40// reached returns true if current time is at or past t.
41func (t fasttime) reached() bool {
42 return fast.current.read() >= t
43}
44
45// makeDeadline returns a time that is approximately time.Now().Add(d)
46func makeDeadline(d time.Duration) fasttime {
47 // Increase the deadline since the clock we are reading may be
48 // just about to tick forwards.
49 end := fast.current.read() + durationToTicks(d+clockPeriod)
50
51 // Start or extend clock if necessary.
52 if end > fast.clockEnd.read() {
53 extendClock(end)
54 }
55 return end
56}
57
58// extendClock ensures that clock is live and will run until at least end.
59func extendClock(end fasttime) {
60 fast.mu.Lock()
61 defer fast.mu.Unlock()
62
63 if fast.start.IsZero() {
64 fast.start = time.Now()
65 }
66
67 // Extend the running time to cover end as well as a bit of slop.
68 if shutdown := end + durationToTicks(time.Second); shutdown > fast.clockEnd.read() {
69 fast.clockEnd.write(shutdown)
70 }
71
72 // Start clock if necessary
73 if !fast.running {
74 fast.running = true
75 go runClock()
76 }
77}
78
79// stop the timeout clock in the background
80// should only used for unit tests to abandon the background goroutine
81func stopClock() {
82 fast.mu.Lock()
83 if fast.running {
84 fast.clockEnd.write(fasttime(0))
85 }
86 fast.mu.Unlock()
87
88 // pause until not running
89 // get and release the lock
90 isRunning := true
91 for isRunning {
92 time.Sleep(clockPeriod / 2)
93 fast.mu.Lock()
94 isRunning = fast.running
95 fast.mu.Unlock()
96 }
97}
98
99func durationToTicks(d time.Duration) fasttime {
100 // Downscale nanoseconds to approximately a millisecond so that we can avoid
101 // overflow even if the caller passes in math.MaxInt64.
102 return fasttime(d) >> 20
103}
104
105const DefaultClockPeriod = 100 * time.Millisecond
106
107// clockPeriod is the approximate interval between updates of approximateClock.
108var clockPeriod = DefaultClockPeriod
109
110func runClock() {
111 fast.mu.Lock()
112 defer fast.mu.Unlock()
113
114 for fast.current.read() <= fast.clockEnd.read() {
115 // Unlock while sleeping.
116 fast.mu.Unlock()
117 time.Sleep(clockPeriod)
118 fast.mu.Lock()
119
120 newTime := durationToTicks(time.Since(fast.start))
121 fast.current.write(newTime)
122 }
123 fast.running = false
124}
125
126type atomicTime struct{ v int64 } // Should change to atomic.Int64 when we can use go 1.19
127
128func (t *atomicTime) read() fasttime { return fasttime(atomic.LoadInt64(&t.v)) }
129func (t *atomicTime) write(v fasttime) { atomic.StoreInt64(&t.v, int64(v)) }