I'm Hanchin Hsieh, a developer passionate about FOSS, and a big Fan of LiSA(織部 里沙). You can ask me about Go, CloudNative, Nvim, and Asahi Linux.
timefall is a generic, high-performance hierarchical timer wheel implementation for Go.
It is suitable for scenarios that require efficient management of a large number of timers with varying expiration times.
The library is externally driven — it does not run its own background loop or goroutine.
Instead, the user is responsible for periodically advancing the timer wheel by calling Update.
When Update is called, all expired timers for the current tick are dispatched to the provided callback.
❯ go test -bench=. -benchmem -count=6 -run=$$ -v
goos: linux
goarch: amd64
pkg: go.yuchanns.xyz/timefall
cpu: Intel(R) Core(TM) i5-10500 CPU @ 3.10GHz
BenchmarkTimerMassive
BenchmarkTimerMassive-12 1977 509834 ns/op 0 B/op 0 allocs/op
BenchmarkTimerMassive-12 2451 550316 ns/op 0 B/op 0 allocs/op
BenchmarkTimerMassive-12 2028 585764 ns/op 0 B/op 0 allocs/op
BenchmarkTimerMassive-12 2188 567332 ns/op 0 B/op 0 allocs/op
BenchmarkTimerMassive-12 2107 509195 ns/op 0 B/op 0 allocs/op
BenchmarkTimerMassive-12 2427 548307 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive
BenchmarkStdTimerMassive-12 72 21694044 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive-12 76 18379989 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive-12 74 24394464 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive-12 74 22217037 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive-12 70 22525907 ns/op 0 B/op 0 allocs/op
BenchmarkStdTimerMassive-12 78 23944621 ns/op 0 B/op 0 allocs/op
PASS
ok go.yuchanns.xyz/timefall 307.905sgo get go.yuchanns.xyz/timefallpackage main
import (
"fmt"
"time"
"go.yuchanns.xyz/timefall"
)
type Event struct {
ID int
}
func main() {
// Create a new timer wheel with a precision of 10ms
tf := timefall.New[Event](10 * time.Millisecond)
// Add a timer that expires after 500ms
tf.Add(&Event{ID: 1}, 500*time.Millisecond)
// Drive the timer wheel using a ticker
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
tf.Update(func(e *Event) {
fmt.Printf("Timer fired: %+v\n", *e)
})
}
}tf := timefall.New[string](time.Millisecond * 5)
tf.Add(newString("A"), 100*time.Millisecond)
tf.Add(newString("B"), 250*time.Millisecond)
tf.Add(newString("C"), 1*time.Second)
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
tf.Update(func(s *string) {
fmt.Println("Expired:", *s)
})
}// Precision of 1ms
tf := timefall.New[int](time.Millisecond * 1)The hierarchical structure efficiently handles timers scheduled far in the future.
tf := timefall.New[string](10 * time.Millisecond)
tf.Add(newString("far future"), 10*time.Second)By default, timer nodes are allocated from the Go heap.
For GC-free operation, a custom allocator can be set via timefall.SetAllocator.
type Allocator interface {
Alloc(size uint) unsafe.Pointer
Free(ptr unsafe.Pointer)
}
func main() {
var myAlloc Allocator = NewMyAllocator()
timefall.SetAllocator(myAlloc)
tf := timefall.New[int](time.Millisecond * 10)
// Timers will now use the custom allocator for node memory
}timefall does not schedule or run timers itself.Update to process expired timers.Licensed under the Apache License, Version 2.0.
Contributions are welcome. Please ensure that any changes maintain the zero-allocation guarantee and thread-safety properties.