130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
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)) }
|