fastclock.go

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