blob: 42ef6d3217f23e98042ec13ae0e89420f7500649 [file] [log] [blame]
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package time_test
import (
"fmt"
"runtime"
"sync"
"testing"
. "time"
)
func TestTicker(t *testing.T) {
t.Parallel()
// We want to test that a ticker takes as much time as expected.
// Since we don't want the test to run for too long, we don't
// want to use lengthy times. This makes the test inherently flaky.
// Start with a short time, but try again with a long one if the
// first test fails.
baseCount := 10
baseDelta := 20 * Millisecond
// On Darwin ARM64 the tick frequency seems limited. Issue 35692.
if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && runtime.GOARCH == "arm64" {
// The following test will run ticker count/2 times then reset
// the ticker to double the duration for the rest of count/2.
// Since tick frequency is limited on Darwin ARM64, use even
// number to give the ticks more time to let the test pass.
// See CL 220638.
baseCount = 6
baseDelta = 100 * Millisecond
}
var errs []string
logErrs := func() {
for _, e := range errs {
t.Log(e)
}
}
for _, test := range []struct {
count int
delta Duration
}{{
count: baseCount,
delta: baseDelta,
}, {
count: 8,
delta: 1 * Second,
}} {
count, delta := test.count, test.delta
ticker := NewTicker(delta)
t0 := Now()
for range count / 2 {
<-ticker.C
}
ticker.Reset(delta * 2)
for range count - count/2 {
<-ticker.C
}
ticker.Stop()
t1 := Now()
dt := t1.Sub(t0)
target := 3 * delta * Duration(count/2)
slop := target * 3 / 10
if dt < target-slop || dt > target+slop {
errs = append(errs, fmt.Sprintf("%d %s ticks then %d %s ticks took %s, expected [%s,%s]", count/2, delta, count/2, delta*2, dt, target-slop, target+slop))
if dt > target+slop {
// System may be overloaded; sleep a bit
// in the hopes it will recover.
Sleep(Second / 2)
}
continue
}
// Now test that the ticker stopped.
Sleep(2 * delta)
select {
case <-ticker.C:
errs = append(errs, "Ticker did not shut down")
continue
default:
// ok
}
// Test passed, so all done.
if len(errs) > 0 {
t.Logf("saw %d errors, ignoring to avoid flakiness", len(errs))
logErrs()
}
return
}
t.Errorf("saw %d errors", len(errs))
logErrs()
}
// Issue 21874
func TestTickerStopWithDirectInitialization(t *testing.T) {
c := make(chan Time)
tk := &Ticker{C: c}
tk.Stop()
}
// Test that a bug tearing down a ticker has been fixed. This routine should not deadlock.
func TestTeardown(t *testing.T) {
t.Parallel()
Delta := 100 * Millisecond
if testing.Short() {
Delta = 20 * Millisecond
}
for range 3 {
ticker := NewTicker(Delta)
<-ticker.C
ticker.Stop()
}
}
// Test the Tick convenience wrapper.
func TestTick(t *testing.T) {
// Test that giving a negative duration returns nil.
if got := Tick(-1); got != nil {
t.Errorf("Tick(-1) = %v; want nil", got)
}
}
// Test that NewTicker panics when given a duration less than zero.
func TestNewTickerLtZeroDuration(t *testing.T) {
defer func() {
if err := recover(); err == nil {
t.Errorf("NewTicker(-1) should have panicked")
}
}()
NewTicker(-1)
}
// Test that Ticker.Reset panics when given a duration less than zero.
func TestTickerResetLtZeroDuration(t *testing.T) {
defer func() {
if err := recover(); err == nil {
t.Errorf("Ticker.Reset(0) should have panicked")
}
}()
tk := NewTicker(Second)
tk.Reset(0)
}
func TestLongAdjustTimers(t *testing.T) {
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
t.Skipf("skipping on %s - too slow", runtime.GOOS)
}
t.Parallel()
var wg sync.WaitGroup
defer wg.Wait()
// Build up the timer heap.
const count = 5000
wg.Add(count)
for range count {
go func() {
defer wg.Done()
Sleep(10 * Microsecond)
}()
}
for range count {
Sleep(1 * Microsecond)
}
// Give ourselves 60 seconds to complete.
// This used to reliably fail on a Mac M3 laptop,
// which needed 77 seconds.
// Trybots are slower, so it will fail even more reliably there.
// With the fix, the code runs in under a second.
done := make(chan bool)
AfterFunc(60*Second, func() { close(done) })
// Set up a queing goroutine to ping pong through the scheduler.
inQ := make(chan func())
outQ := make(chan func())
defer close(inQ)
wg.Add(1)
go func() {
defer wg.Done()
defer close(outQ)
var q []func()
for {
var sendTo chan func()
var send func()
if len(q) > 0 {
sendTo = outQ
send = q[0]
}
select {
case sendTo <- send:
q = q[1:]
case f, ok := <-inQ:
if !ok {
return
}
q = append(q, f)
case <-done:
return
}
}
}()
for i := range 50000 {
const try = 20
for range try {
inQ <- func() {}
}
for range try {
select {
case _, ok := <-outQ:
if !ok {
t.Fatal("output channel is closed")
}
case <-After(5 * Second):
t.Fatalf("failed to read work, iteration %d", i)
case <-done:
t.Fatal("timer expired")
}
}
}
}
func BenchmarkTicker(b *testing.B) {
benchmark(b, func(pb *testing.PB) {
ticker := NewTicker(Nanosecond)
for pb.Next() {
<-ticker.C
}
ticker.Stop()
})
}
func BenchmarkTickerReset(b *testing.B) {
benchmark(b, func(pb *testing.PB) {
ticker := NewTicker(Nanosecond)
for pb.Next() {
ticker.Reset(Nanosecond * 2)
}
ticker.Stop()
})
}
func BenchmarkTickerResetNaive(b *testing.B) {
benchmark(b, func(pb *testing.PB) {
ticker := NewTicker(Nanosecond)
for pb.Next() {
ticker.Stop()
ticker = NewTicker(Nanosecond * 2)
}
ticker.Stop()
})
}
func TestTimerGC(t *testing.T) {
run := func(t *testing.T, what string, f func()) {
t.Helper()
t.Run(what, func(t *testing.T) {
t.Helper()
const N = 1e4
var stats runtime.MemStats
runtime.GC()
runtime.GC()
runtime.GC()
runtime.ReadMemStats(&stats)
before := int64(stats.Mallocs - stats.Frees)
for j := 0; j < N; j++ {
f()
}
runtime.GC()
runtime.GC()
runtime.GC()
runtime.ReadMemStats(&stats)
after := int64(stats.Mallocs - stats.Frees)
// Allow some slack, but inuse >= N means at least 1 allocation per iteration.
inuse := after - before
if inuse >= N {
t.Errorf("%s did not get GC'ed: %d allocations", what, inuse)
Sleep(1 * Second)
runtime.ReadMemStats(&stats)
after := int64(stats.Mallocs - stats.Frees)
inuse = after - before
t.Errorf("after a sleep: %d allocations", inuse)
}
})
}
run(t, "After", func() { After(Hour) })
run(t, "Tick", func() { Tick(Hour) })
run(t, "NewTimer", func() { NewTimer(Hour) })
run(t, "NewTicker", func() { NewTicker(Hour) })
run(t, "NewTimerStop", func() { NewTimer(Hour).Stop() })
run(t, "NewTickerStop", func() { NewTicker(Hour).Stop() })
}
func TestChan(t *testing.T) {
for _, name := range []string{"0", "1", "2"} {
t.Run("asynctimerchan="+name, func(t *testing.T) {
t.Setenv("GODEBUG", "asynctimerchan="+name)
t.Run("Timer", func(t *testing.T) {
tim := NewTimer(10000 * Second)
testTimerChan(t, tim, tim.C, name == "0")
})
t.Run("Ticker", func(t *testing.T) {
tim := &tickerTimer{Ticker: NewTicker(10000 * Second)}
testTimerChan(t, tim, tim.C, name == "0")
})
})
}
}
type timer interface {
Stop() bool
Reset(Duration) bool
}
// tickerTimer is a Timer with Reset and Stop methods that return bools,
// to have the same signatures as Timer.
type tickerTimer struct {
*Ticker
stopped bool
}
func (t *tickerTimer) Stop() bool {
pending := !t.stopped
t.stopped = true
t.Ticker.Stop()
return pending
}
func (t *tickerTimer) Reset(d Duration) bool {
pending := !t.stopped
t.stopped = false
t.Ticker.Reset(d)
return pending
}
func testTimerChan(t *testing.T, tim timer, C <-chan Time, synctimerchan bool) {
_, isTimer := tim.(*Timer)
isTicker := !isTimer
// Retry parameters. Enough to deflake even on slow machines.
// Windows in particular has very coarse timers so we have to
// wait 10ms just to make a timer go off.
const (
sched = 10 * Millisecond
tries = 100
drainTries = 5
)
drain := func() {
for range drainTries {
select {
case <-C:
return
default:
}
Sleep(sched)
}
}
noTick := func() {
t.Helper()
select {
default:
case <-C:
t.Errorf("extra tick")
}
}
assertTick := func() {
t.Helper()
select {
default:
case <-C:
return
}
for range tries {
Sleep(sched)
select {
default:
case <-C:
return
}
}
t.Errorf("missing tick")
}
assertLen := func() {
t.Helper()
if synctimerchan {
if n := len(C); n != 0 {
t.Errorf("synctimer has len(C) = %d, want 0 (always)", n)
}
return
}
var n int
if n = len(C); n == 1 {
return
}
for range tries {
Sleep(sched)
if n = len(C); n == 1 {
return
}
}
t.Errorf("len(C) = %d, want 1", n)
}
// Test simple stop; timer never in heap.
tim.Stop()
noTick()
// Test modify of timer not in heap.
tim.Reset(10000 * Second)
noTick()
if synctimerchan {
// Test modify of timer in heap.
tim.Reset(1)
Sleep(sched)
if l, c := len(C), cap(C); l != 0 || c != 0 {
//t.Fatalf("len(C), cap(C) = %d, %d, want 0, 0", l, c)
}
assertTick()
} else {
// Test modify of timer in heap.
tim.Reset(1)
assertTick()
Sleep(sched)
tim.Reset(10000 * Second)
if isTicker {
assertTick()
}
noTick()
// Test that len sees an immediate tick arrive
// for Reset of timer in heap.
tim.Reset(1)
assertLen()
assertTick()
// Test that len sees an immediate tick arrive
// for Reset of timer NOT in heap.
tim.Stop()
if !synctimerchan {
drain()
}
tim.Reset(1)
assertLen()
assertTick()
}
// Sleep long enough that a second tick must happen if this is a ticker.
// Test that Reset does not lose the tick that should have happened.
Sleep(sched)
tim.Reset(10000 * Second)
if !synctimerchan && isTicker {
assertLen()
assertTick()
}
noTick()
notDone := func(done chan bool) {
t.Helper()
select {
default:
case <-done:
t.Fatalf("early done")
}
}
waitDone := func(done chan bool) {
t.Helper()
for range tries {
Sleep(sched)
select {
case <-done:
return
default:
}
}
t.Fatalf("never got done")
}
// Reset timer in heap (already reset above, but just in case).
tim.Reset(10000 * Second)
if !synctimerchan {
drain()
}
// Test stop while timer in heap (because goroutine is blocked on <-C).
done := make(chan bool)
notDone(done)
go func() {
<-C
close(done)
}()
Sleep(sched)
notDone(done)
// Test reset far away while timer in heap.
tim.Reset(20000 * Second)
Sleep(sched)
notDone(done)
// Test imminent reset while in heap.
tim.Reset(1)
waitDone(done)
// If this is a ticker, another tick should have come in already
// (they are 1ns apart). If a timer, it should have stopped.
if isTicker {
assertTick()
} else {
noTick()
}
tim.Stop()
if isTicker || !synctimerchan {
t.Logf("drain")
drain()
}
noTick()
// Again using select and with two goroutines waiting.
tim.Reset(10000 * Second)
if !synctimerchan {
drain()
}
done = make(chan bool, 2)
done1 := make(chan bool)
done2 := make(chan bool)
stop := make(chan bool)
go func() {
select {
case <-C:
done <- true
case <-stop:
}
close(done1)
}()
go func() {
select {
case <-C:
done <- true
case <-stop:
}
close(done2)
}()
Sleep(sched)
notDone(done)
tim.Reset(sched / 2)
Sleep(sched)
waitDone(done)
tim.Stop()
close(stop)
waitDone(done1)
waitDone(done2)
if isTicker {
// extra send might have sent done again
// (handled by buffering done above).
select {
default:
case <-done:
}
// extra send after that might have filled C.
select {
default:
case <-C:
}
}
notDone(done)
// Test enqueueTimerChan when timer is stopped.
stop = make(chan bool)
done = make(chan bool, 2)
for range 2 {
go func() {
select {
case <-C:
panic("unexpected data")
case <-stop:
}
done <- true
}()
}
Sleep(sched)
close(stop)
waitDone(done)
waitDone(done)
// Test that Stop and Reset block old values from being received.
// (Proposal go.dev/issue/37196.)
if synctimerchan {
tim.Reset(1)
Sleep(10 * Millisecond)
if pending := tim.Stop(); pending != true {
t.Errorf("tim.Stop() = %v, want true", pending)
}
noTick()
tim.Reset(Hour)
noTick()
if pending := tim.Reset(1); pending != true {
t.Errorf("tim.Stop() = %v, want true", pending)
}
assertTick()
Sleep(10 * Millisecond)
if isTicker {
assertTick()
Sleep(10 * Millisecond)
} else {
noTick()
}
if pending, want := tim.Reset(Hour), isTicker; pending != want {
t.Errorf("tim.Stop() = %v, want %v", pending, want)
}
noTick()
}
}
func TestManualTicker(t *testing.T) {
// Code should not do this, but some old code dating to Go 1.9 does.
// Make sure this doesn't crash.
// See go.dev/issue/21874.
c := make(chan Time)
tick := &Ticker{C: c}
tick.Stop()
}
func TestAfterTimes(t *testing.T) {
t.Parallel()
// Using After(10ms) but waiting for 500ms to read the channel
// should produce a time from start+10ms, not start+500ms.
// Make sure it does.
// To avoid flakes due to very long scheduling delays,
// require 10 failures in a row before deciding something is wrong.
for range 10 {
start := Now()
c := After(10 * Millisecond)
Sleep(500 * Millisecond)
dt := (<-c).Sub(start)
if dt < 400*Millisecond {
return
}
t.Logf("After(10ms) time is +%v, want <400ms", dt)
}
t.Errorf("not working")
}
func TestTickTimes(t *testing.T) {
t.Parallel()
// See comment in TestAfterTimes
for range 10 {
start := Now()
c := Tick(10 * Millisecond)
Sleep(500 * Millisecond)
dt := (<-c).Sub(start)
if dt < 400*Millisecond {
return
}
t.Logf("Tick(10ms) time is +%v, want <400ms", dt)
}
t.Errorf("not working")
}