I think you need to establish for yourself the acceptable lower and upper bounds for your program's timing needs, and then over-sample and test for the correct interval:
const (
lower = 19_900 * time.Microsecond
upper = 20_100 * time.Microsecond
)
// pulse does something every 20ms +/-.1ms
func pulse() {
var (
last = time.Now()
elapsed time.Duration
)
ticker := time.NewTicker(500 * time.Microsecond)
for now := range ticker.C {
elapsed = now.Sub(last)
if elapsed > lower {
// Some procedure...
last = now
}
}
}
Even though the ticker's period of 500µs is coarser than the margin of +/- 100µs, I haven't seen interval errors in a test run of 1000 pulses (20s).
I don't know much about Go's scheduler, and the internals of timing. I read this issue on the performance of Sleep and Ticker, and that gave me some ideas about performance and patterns. This SO, Sleep() or Timer(), Go idiomatic?, gave me the idea that I should probably just use Timer.
I've included a complete program to generate about 1000 test pulses and some stats like:
...5s elapsed
...5s elapsed
...5s elapsed
...5s elapsed
1000 pulses: 19.941ms...20.061916ms
Some times (like launching the Activity Monitor in macOS, mid-run), my system was stressed enough to be late by a factor of 3:
...
9 too-long pulses:
20.173042ms
20.294166ms
20.314125ms
20.333667ms
20.611917ms
21.762083ms
22.614583ms
22.809292ms
58.68725ms
Usually, though, even with the CPU pegged at 100%, the Ticker was within my 100µs bounds.
func main() {
done := time.After(20*time.Second + 1*time.Millisecond) // run time limit
tickTock := time.NewTicker(5 * time.Second) // incremental print msg
go randomWork() // pegs CPU at about 100%
chP := make(chan time.Duration)
go pulse(chP)
pulses := make([]time.Duration, 0)
loop:
for {
select {
case d := <-chP:
pulses = append(pulses, d)
case <-tickTock.C:
fmt.Println("...5s elapsed")
case <-done:
break loop
}
}
printStats(pulses)
}
const (
lower = 19_900 * time.Microsecond
upper = 20_100 * time.Microsecond
)
// pulse does something every 20ms +/-.1ms
func pulse(ch chan<- time.Duration) {
var (
last = time.Now()
elapsed time.Duration
)
ticker := time.NewTicker(500 * time.Microsecond)
for now := range ticker.C {
elapsed = now.Sub(last)
if elapsed > lower {
// Some procedure...
ch <- elapsed
last = now
}
}
}
func randomWork() {
for {
x := 0
for i := 0; i < rand.Intn(1e7); i++ {
x = i
}
x = x // silence "unused x" error
}
}
func printStats(pulses []time.Duration) {
sort.Slice(pulses, func(i, j int) bool { return pulses[i] < pulses[j] })
tooShort := make([]time.Duration, 0)
tooLong := make([]time.Duration, 0)
for _, d := range pulses {
if d < lower {
tooShort = append(tooShort, d)
}
if d > upper {
tooLong = append(tooLong, d)
}
}
fmt.Printf("%d pulses: %v...%v\n", len(pulses), pulses[0], pulses[len(pulses)-1])
if len(tooShort) > 0 {
fmt.Printf("%d too-short pulses:\n", len(tooShort))
for _, d := range tooShort {
fmt.Printf(" %v\n", d)
}
}
if len(tooLong) > 0 {
fmt.Printf("%d too-long pulses:\n", len(tooLong))
for _, d := range tooLong {
fmt.Printf(" %v\n", d)
}
}
}