2

Not able to find out where in the below for loop we are spending more than ten microseconds so that we are missing vast number of ticks?

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    RunTicker(time.Millisecond, 10 * time.Second) // Scenario 1
    RunTicker(10 * time.Microsecond, 10 * time.Second) // Scenario 2
}

func RunTicker(tickerInterval, tickerDuration time.Duration) {
    var counter int

    ctx, can := context.WithTimeout(context.Background(), tickerDuration)
    defer can()

    ticker := time.NewTicker(tickerInterval)

exitfor:
    for {
        select {
        case <-ticker.C:
            counter++
        case <- ctx.Done():
            ticker.Stop()
            break exitfor
        }
    }

    fmt.Printf("Tick interval %v and running for %v.Expected counter: %d but got %d\n", tickerInterval, tickerDuration, tickerDuration/tickerInterval, counter)
}

Output:

Tick interval 1ms and running for 10s.Expected counter: 10000 but got 9965
Tick interval 10µs and running for 10s.Expected counter: 1000000 but got 976590
chanchal1987
  • 2,320
  • 7
  • 31
  • 64
  • 1
    I wouldn't call it a "vast number" that you are missing. In your tests you are getting 99% and 97% of the expected values. – Hymns For Disco Jul 02 '21 at 06:37
  • Thank you for your response. But my question is what is stopping to get 100% expected values? – chanchal1987 Jul 02 '21 at 06:40
  • That should be the `counter++` delay – nipuna Jul 02 '21 at 06:57
  • I also thought that. So I did benchmark that statement only. It was not taking more than few nanoseconds. – chanchal1987 Jul 02 '21 at 07:50
  • 2
    Depending on the OS and hardware, the system clock may not offer microsecond precision, or even single-millisecond precision. Go's timing system is naturally limited by the timing system of the operating environment. – Adrian Jul 02 '21 at 14:10

1 Answers1

8

In general, when an API says that an event will take X seconds, it's actually guaranteeing that the elapsed time will be at least X seconds, and not less. Especially for small time increments, this is an important distinction.

Also, consider this point from the NewTicker documentation:

The period of the ticks is specified by the duration argument. The ticker will adjust the time interval or drop ticks to make up for slow receivers.

With these two points in mind, the only guarantee you really have is that the number of real ticks will be <= to the expected number you calculate, and no more. In other words, actual ticks == expected ticks only in the ideal case, and every other case will be less than that.

In these kinds of small time increments (~< 1ms), there could be other events besides "user code" that exceed the tick time including:

  • Goroutine scheduling logic (sleeping and resuming goroutines, thread switching).
  • Garbage collection (even if there's no garbage made during the loop, the GC is probably still "alive" and will occasionally check for garbage)

These other factors can coincide, making it even more likely that a tick will be skipped or delayed.

Think of it like you have a bucket full of water, and you need to pour it into another bucket, and then another bucket, and another one and so on for 1000s of buckets. The only thing you can do is lose water, and you can't gain any more once it's spilled. With this scenario, you would never expect to retain 100% of the water to the very end.

This is similar to the case you mention because the errors only go in one direction. Delays can only be at least the specified time, and ticks can only be lost (extras are never gained). Any time any of these events happen, it's like a drop of water is lost.

Hymns For Disco
  • 7,530
  • 2
  • 17
  • 33
  • I am probably still missing something. As per my understanding from the documentation, ticker should only drop the ticks if receiver is slower than the tick interval. In this case the receiver is only running one statement counter++ which should not spend more than 1ms. I also checked the GC stats in my machine which may potentially decrease the performance of the increment statement, but no GC was executed during the run time. – chanchal1987 Jul 02 '21 at 08:14
  • 4
    Even if no ticks are dropped, the first point is still important that time guarantees are only "at least this much time", not "exactly this much time". On my machine, for example, putting the ticker time any lower than ~15ms has no effect (it won't get any faster than 15ms). In practical terms, there will always be a time interval small enough that you can't accurately delay for that amount of time. What level of accuracy can be maintained at small intervals would probably depend on the hardware / OS environment. – Hymns For Disco Jul 02 '21 at 08:37