├── .github ├── FUNDING.yml ├── demo.gif ├── logo.pdn ├── logo.png └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── emit ├── README.md ├── event.go └── event_test.go ├── example ├── event │ └── main.go └── timeline │ └── main.go ├── go.mod ├── go.sum ├── timeline.go └── timeline_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kelindar] 2 | -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelindar/timeline/db19af96b43bc2da9b25764c984800a54faf87c3/.github/demo.gif -------------------------------------------------------------------------------- /.github/logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelindar/timeline/db19af96b43bc2da9b25764c984800a54faf87c3/.github/logo.pdn -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelindar/timeline/db19af96b43bc2da9b25764c984800a54faf87c3/.github/logo.png -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | env: 4 | GITHUB_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 5 | GO111MODULE: "on" 6 | jobs: 7 | test: 8 | name: Test with Coverage 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set up Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: "1.20" 15 | - name: Check out code 16 | uses: actions/checkout@v2 17 | - name: Install dependencies 18 | run: | 19 | go mod download 20 | - name: Run Unit Tests 21 | run: | 22 | go test -tags noasm -race -covermode atomic -coverprofile=profile.cov ./... 23 | go test -race ./... 24 | - name: Upload Coverage 25 | uses: shogo82148/actions-goveralls@v1 26 | with: 27 | path-to-profile: profile.cov 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Roman Atachiants 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | kelindar/timeline 3 |
4 | Go Version 5 | PkgGoDev 6 | Go Report Card 7 | License 8 | Coverage 9 |

10 | 11 | ## Timeline: High-Performance Task Scheduling in Go 12 | This library provides a **high-performance, in-memory task scheduler** for Go, designed for precise and efficient time-based task management. It uses a 10ms resolution and a bucketing system for scalable scheduling, making it ideal for real-time and concurrent applications. 13 | 14 | - **High Performance:** Optimized for rapid scheduling and execution of thousands of tasks per second, with minimal overhead. 15 | - **Fine-Grained Precision:** Schedules tasks with 10ms accuracy, suitable for high-frequency or real-time workloads. 16 | - **Efficient Memory Use:** Predictable, linear memory consumption thanks to its bucketing design. 17 | - **Thread-Safe:** Safe for concurrent use, supporting multi-threaded scheduling and execution. 18 | 19 | ![demo](./.github/demo.gif) 20 | 21 | **Use When:** 22 | - ✅ Scheduling frequent, short-lived tasks (e.g., game loops, real-time updates). 23 | - ✅ Requiring precise, low-latency task execution within a single Go process. 24 | - ✅ Building systems where predictable memory and performance are critical. 25 | - ✅ Needing a simple, dependency-free scheduler for in-process workloads. 26 | 27 | **Not For:** 28 | - ❌ Long-term scheduling (tasks days/weeks in advance). 29 | - ❌ Applications with highly sporadic or infrequent task scheduling (due to required ticking). 30 | 31 | ## Quick Start 32 | 33 | ```go 34 | // Initialize the scheduler and start the internal clock 35 | scheduler := timeline.New() 36 | cancel := scheduler.Start(context.Background()) 37 | defer cancel() // Call this to stop the scheduler's internal clock 38 | 39 | // Define a task 40 | task := func(now time.Time, elapsed time.Duration) bool { 41 | fmt.Printf("Task executed at %d:%02d.%03d, elapsed=%v\n", 42 | now.Hour(), now.Second(), now.UnixMilli()%1000, elapsed) 43 | return true // return true to keep the task scheduled 44 | } 45 | 46 | // Schedule the task to run immediately 47 | scheduler.Run(task) 48 | 49 | // Schedule the task to run every second 50 | scheduler.RunEvery(task, 1*time.Second) 51 | 52 | // Schedule the task to run after 5 seconds 53 | scheduler.RunAfter(task, 5*time.Second) 54 | 55 | // Let the scheduler run for 10 seconds 56 | time.Sleep(10 * time.Second) 57 | ``` 58 | 59 | It outputs: 60 | 61 | ``` 62 | Task executed at 04.400, elapsed=0s 63 | Task executed at 05.000, elapsed=600ms 64 | Task executed at 06.000, elapsed=1s 65 | Task executed at 07.000, elapsed=1s 66 | Task executed at 08.000, elapsed=1s 67 | Task executed at 09.000, elapsed=1s 68 | Task executed at 09.400, elapsed=5s 69 | Task executed at 10.000, elapsed=1s 70 | Task executed at 11.000, elapsed=1s 71 | Task executed at 12.000, elapsed=1s 72 | Task executed at 13.000, elapsed=1s 73 | Task executed at 14.000, elapsed=1s 74 | ``` 75 | 76 | ## Event Scheduling (Integration) 77 | 78 | The [github.com/kelindar/timeline/emit](https://github.com/kelindar/timeline/tree/main/emit) sub-package seamlessly integrates the timeline scheduler with event-driven programming. It allows you to emit and subscribe to events with precise timing, making it ideal for applications that require both event-driven architectures and time-based scheduling. 79 | 80 | ```go 81 | // Custom event type 82 | type Message struct { 83 | Text string 84 | } 85 | 86 | // Type returns the type of the event for the dispatcher 87 | func (Message) Type() uint32 { 88 | return 0x1 89 | } 90 | 91 | func main() { 92 | 93 | // Emit the event immediately 94 | event.Next(Message{Text: "Hello, World!"}) 95 | 96 | // Emit the event every second 97 | event.Every(Message{Text: "Are we there yet?"}, 500*time.Millisecond) 98 | 99 | // Subscribe and Handle the Event 100 | cancel := event.On(func(ev Message, now time.Time, elapsed time.Duration) error { 101 | fmt.Printf("Received '%s' at %02d.%03d, elapsed=%v\n", 102 | ev.Text, 103 | now.Second(), now.UnixMilli()%1000, elapsed) 104 | return nil 105 | }) 106 | defer cancel() // Remember to unsubscribe when done 107 | 108 | // Let the program run for a while to receive events 109 | time.Sleep(5 * time.Second) 110 | } 111 | ``` 112 | 113 | The example above demonstrates how to create a custom event type, emit events, and subscribe to them using the timeline scheduler. It outputs: 114 | 115 | ``` 116 | Received 'Hello, World!' at 19.580, elapsed=0s 117 | Received 'Are we there yet?' at 20.000, elapsed=420ms 118 | Received 'Are we there yet?' at 20.500, elapsed=500ms 119 | Received 'Are we there yet?' at 21.000, elapsed=500ms 120 | Received 'Are we there yet?' at 21.500, elapsed=500ms 121 | Received 'Are we there yet?' at 22.000, elapsed=500ms 122 | Received 'Are we there yet?' at 22.500, elapsed=500ms 123 | Received 'Are we there yet?' at 23.000, elapsed=500ms 124 | Received 'Are we there yet?' at 23.500, elapsed=500ms 125 | Received 'Are we there yet?' at 24.000, elapsed=500ms 126 | Received 'Are we there yet?' at 24.500, elapsed=500ms 127 | ``` 128 | 129 | -------------------------------------------------------------------------------- /emit/README.md: -------------------------------------------------------------------------------- 1 | ## Event Package for Timeline 2 | 3 | The event package seamlessly integrates the timeline scheduler with event-driven programming. It allows you to emit and subscribe to events with precise timing, making it ideal for applications that require both event-driven architectures and time-based scheduling. 4 | 5 | ## Quick Start 6 | 7 | Let's dive right in with a simple example to get you started with the event package. 8 | 9 | ```go 10 | // Custom event type 11 | type Message struct { 12 | Text string 13 | } 14 | 15 | // Type returns the type of the event for the dispatcher 16 | func (Message) Type() uint32 { 17 | return 0x1 18 | } 19 | 20 | func main() { 21 | 22 | // Emit the event immediately 23 | event.Next(Message{Text: "Hello, World!"}) 24 | 25 | // Emit the event every second 26 | event.Every(Message{Text: "Are we there yet?"}, 1*time.Second) 27 | 28 | // Subscribe and Handle the Event 29 | cancel := event.On[Message](func(ev Message, now time.Time, elapsed time.Duration) error { 30 | fmt.Printf("Received '%s' at %02d.%03d, elapsed=%v\n", 31 | ev.Text, 32 | now.Second(), now.UnixMilli()%1000, elapsed) 33 | return nil 34 | }) 35 | defer cancel() // Remember to unsubscribe when done 36 | 37 | // Let the program run for a while to receive events 38 | time.Sleep(5 * time.Second) 39 | } 40 | 41 | ``` 42 | 43 | You will see similar output, with 'Are we there yet?' being emitted every second, and 'Hello, World!' being emitted immediately. 44 | 45 | ``` 46 | Received 'Hello, World!' at 21.060, elapsed=0s 47 | Received 'Are we there yet?' at 22.000, elapsed=940ms 48 | Received 'Are we there yet?' at 23.000, elapsed=1s 49 | Received 'Are we there yet?' at 24.000, elapsed=1s 50 | Received 'Are we there yet?' at 25.000, elapsed=1s 51 | Received 'Are we there yet?' at 26.000, elapsed=1s 52 | ``` 53 | -------------------------------------------------------------------------------- /emit/event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root 3 | 4 | package emit 5 | 6 | import ( 7 | "context" 8 | "math" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/kelindar/event" 13 | "github.com/kelindar/timeline" 14 | ) 15 | 16 | // Scheduler is the default scheduler used to emit events. 17 | var Scheduler = func() *timeline.Scheduler { 18 | s := timeline.New() 19 | s.Start(context.Background()) 20 | return s 21 | }() 22 | 23 | // ----------------------------------------- Forward Event ----------------------------------------- 24 | 25 | // signal represents a forwarded event 26 | type signal[T event.Event] struct { 27 | Time time.Time // The time at which the event was emitted 28 | Elapsed time.Duration // The time elapsed since the last event 29 | Data T 30 | } 31 | 32 | // Type returns the type of the event 33 | func (e signal[T]) Type() uint32 { 34 | return e.Data.Type() 35 | } 36 | 37 | // ----------------------------------------- Error Event ----------------------------------------- 38 | 39 | // fault represents an error event 40 | type fault struct { 41 | error 42 | About any // The context of the error 43 | } 44 | 45 | // Type returns the type of the event 46 | func (e fault) Type() uint32 { 47 | return math.MaxUint32 48 | } 49 | 50 | // ----------------------------------------- Timer Event ----------------------------------------- 51 | 52 | var nextTimerID uint32 = 1 << 30 53 | 54 | // Timer represents a Timer event 55 | type Timer struct { 56 | ID uint32 57 | } 58 | 59 | // Type returns the type of the event 60 | func (e Timer) Type() uint32 { 61 | return e.ID 62 | } 63 | 64 | // ----------------------------------------- Subscribe ----------------------------------------- 65 | 66 | // On subscribes to an event, the type of the event will be automatically 67 | // inferred from the provided type. Must be constant for this to work. 68 | func On[T event.Event](handler func(event T, now time.Time, elapsed time.Duration) error) context.CancelFunc { 69 | return event.Subscribe[signal[T]](event.Default, func(m signal[T]) { 70 | if err := handler(m.Data, m.Time, m.Elapsed); err != nil { 71 | Error(err, m.Data) 72 | } 73 | }) 74 | } 75 | 76 | // OnType subscribes to an event with the specified event type. 77 | func OnType[T event.Event](eventType uint32, handler func(event T, now time.Time, elapsed time.Duration) error) context.CancelFunc { 78 | return event.SubscribeTo[signal[T]](event.Default, eventType, func(m signal[T]) { 79 | if err := handler(m.Data, m.Time, m.Elapsed); err != nil { 80 | Error(err, m.Data) 81 | } 82 | }) 83 | } 84 | 85 | // OnError subscribes to an error event. 86 | func OnError(handler func(err error, about any)) context.CancelFunc { 87 | return event.Subscribe[fault](event.Default, func(m fault) { 88 | handler(m.error, m.About) 89 | }) 90 | } 91 | 92 | // OnEvery creates a timer that fires every 'interval' and calls the handler. 93 | func OnEvery(handler func(now time.Time, elapsed time.Duration) error, interval time.Duration) context.CancelFunc { 94 | id := atomic.AddUint32(&nextTimerID, 1) 95 | if id >= (math.MaxUint32 - 1) { 96 | panic("emit: too many timers created") 97 | } 98 | 99 | // Subscribe to the timer event 100 | cancel := OnType[Timer](id, func(_ Timer, now time.Time, elapsed time.Duration) error { 101 | return handler(now, elapsed) 102 | }) 103 | 104 | // Start the timer 105 | Every(Timer{ID: id}, interval) 106 | return cancel 107 | } 108 | 109 | // ----------------------------------------- Publish ----------------------------------------- 110 | 111 | // Next writes an event during the next tick. 112 | func Next[T event.Event](ev T) { 113 | Scheduler.Run(emit(ev)) 114 | } 115 | 116 | // At writes an event at specific 'at' time. 117 | func At[T event.Event](ev T, at time.Time) { 118 | Scheduler.RunAt(emit(ev), at) 119 | } 120 | 121 | // After writes an event after a 'delay'. 122 | func After[T event.Event](ev T, after time.Duration) { 123 | Scheduler.RunAfter(emit(ev), after) 124 | } 125 | 126 | // Every writes an event at 'interval' intervals, starting at the next boundary tick. 127 | // Returns a cancel function to stop the recurring event. 128 | func Every[T event.Event](ev T, interval time.Duration) context.CancelFunc { 129 | return emitEvery(ev, interval, func(task timeline.Task, interval time.Duration) { 130 | Scheduler.RunEvery(task, interval) 131 | }) 132 | } 133 | 134 | // EveryAt writes an event at 'interval' intervals, starting at 'startTime'. 135 | // Returns a cancel function to stop the recurring event. 136 | func EveryAt[T event.Event](ev T, interval time.Duration, startTime time.Time) context.CancelFunc { 137 | return emitEvery(ev, interval, func(task timeline.Task, interval time.Duration) { 138 | Scheduler.RunEveryAt(task, interval, startTime) 139 | }) 140 | } 141 | 142 | // EveryAfter writes an event at 'interval' intervals after a 'delay'. 143 | // Returns a cancel function to stop the recurring event. 144 | func EveryAfter[T event.Event](ev T, interval time.Duration, delay time.Duration) context.CancelFunc { 145 | return emitEvery(ev, interval, func(task timeline.Task, interval time.Duration) { 146 | Scheduler.RunEveryAfter(task, interval, delay) 147 | }) 148 | } 149 | 150 | // Error writes an error event. 151 | func Error(err error, about any) { 152 | event.Publish(event.Default, fault{ 153 | error: err, 154 | About: about, 155 | }) 156 | } 157 | 158 | // emit writes an event into the dispatcher 159 | func emit[T event.Event](ev T) func(now time.Time, elapsed time.Duration) bool { 160 | return func(now time.Time, elapsed time.Duration) bool { 161 | event.Publish(event.Default, signal[T]{ 162 | Data: ev, 163 | Time: now, 164 | Elapsed: elapsed, 165 | }) 166 | return true 167 | } 168 | } 169 | 170 | // emitEvery creates a cancellable recurring event 171 | func emitEvery[T event.Event](ev T, interval time.Duration, scheduler func(timeline.Task, time.Duration)) func() { 172 | var cancelled atomic.Bool 173 | task := func(now time.Time, elapsed time.Duration) bool { 174 | event.Publish(event.Default, signal[T]{ 175 | Data: ev, 176 | Time: now, 177 | Elapsed: elapsed, 178 | }) 179 | return !cancelled.Load() 180 | } 181 | 182 | scheduler(task, interval) 183 | return func() { 184 | cancelled.Store(true) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /emit/event_test.go: -------------------------------------------------------------------------------- 1 | package emit 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | /* 13 | go test -bench=. -benchmem -benchtime=10s 14 | cpu: 13th Gen Intel(R) Core(TM) i7-13700K 15 | BenchmarkEvent/1x1-24 13259682 84.58 ns/op 11.73 million/s 169 B/op 1 allocs/op 16 | BenchmarkEvent/1x10-24 16216171 104.8 ns/op 74.95 million/s 249 B/op 1 allocs/op 17 | BenchmarkEvent/1x100-24 26087012 669.5 ns/op 70.51 million/s 228 B/op 1 allocs/op 18 | BenchmarkEvent/10x1-24 2721086 510.1 ns/op 18.33 million/s 953 B/op 10 allocs/op 19 | BenchmarkEvent/10x10-24 1000000 1095 ns/op 50.99 million/s 2100 B/op 10 allocs/op 20 | BenchmarkEvent/10x100-24 1000000 1294 ns/op 57.49 million/s 2151 B/op 10 allocs/op 21 | */ 22 | func BenchmarkEvent(b *testing.B) { 23 | for _, topics := range []int{1, 10} { 24 | for _, subs := range []int{1, 10, 100} { 25 | b.Run(fmt.Sprintf("%dx%d", topics, subs), func(b *testing.B) { 26 | var count atomic.Int64 27 | for i := 0; i < subs; i++ { 28 | for id := 10; id < 10+topics; id++ { 29 | defer OnType(uint32(id), func(ev Dynamic, now time.Time, elapsed time.Duration) error { 30 | count.Add(1) 31 | return nil 32 | })() 33 | } 34 | } 35 | 36 | b.ReportAllocs() 37 | b.ResetTimer() 38 | 39 | start := time.Now() 40 | for n := 0; n < b.N; n++ { 41 | for id := 10; id < 10+topics; id++ { 42 | Next(Dynamic{ID: id}) 43 | } 44 | } 45 | 46 | elapsed := time.Since(start) 47 | rate := float64(count.Load()) / 1e6 / elapsed.Seconds() 48 | b.ReportMetric(rate, "million/s") 49 | }) 50 | } 51 | } 52 | } 53 | 54 | func TestEmit(t *testing.T) { 55 | events := make(chan MyEvent2) 56 | defer On(func(ev MyEvent2, now time.Time, elapsed time.Duration) error { 57 | assert.Equal(t, "Hello", ev.Text) 58 | events <- ev 59 | return nil 60 | })() 61 | 62 | // Emit the event 63 | Next(MyEvent2{Text: "Hello"}) 64 | <-events 65 | 66 | At(MyEvent2{Text: "Hello"}, time.Now().Add(40*time.Millisecond)) 67 | <-events 68 | 69 | After(MyEvent2{Text: "Hello"}, 20*time.Millisecond) 70 | <-events 71 | 72 | EveryAt(MyEvent2{Text: "Hello"}, 50*time.Millisecond, time.Now().Add(10*time.Millisecond)) 73 | <-events 74 | 75 | EveryAfter(MyEvent2{Text: "Hello"}, 30*time.Millisecond, 10*time.Millisecond) 76 | <-events 77 | 78 | Every(MyEvent2{Text: "Hello"}, 10*time.Millisecond) 79 | <-events 80 | } 81 | 82 | func TestOnType(t *testing.T) { 83 | events := make(chan Dynamic) 84 | defer OnType(42, func(ev Dynamic, now time.Time, elapsed time.Duration) error { 85 | assert.Equal(t, 42, ev.ID) 86 | events <- ev 87 | return nil 88 | })() 89 | 90 | // Emit the event 91 | Next(Dynamic{ID: 42}) 92 | <-events 93 | } 94 | 95 | func TestOnError(t *testing.T) { 96 | errors := make(chan error) 97 | defer OnError(func(err error, about any) { 98 | errors <- err 99 | })() 100 | 101 | defer On(func(ev MyEvent2, now time.Time, elapsed time.Duration) error { 102 | return fmt.Errorf("On()") 103 | })() 104 | 105 | // Emit the event 106 | Error(fmt.Errorf("Err"), nil) 107 | assert.Equal(t, "Err", (<-errors).Error()) 108 | 109 | // Fail in the handler 110 | Next(MyEvent2{}) 111 | assert.Equal(t, "On()", (<-errors).Error()) 112 | 113 | } 114 | 115 | func TestOnTypeError(t *testing.T) { 116 | errors := make(chan error) 117 | defer OnError(func(err error, about any) { 118 | errors <- err 119 | })() 120 | 121 | defer OnType(42, func(ev Dynamic, now time.Time, elapsed time.Duration) error { 122 | return fmt.Errorf("OnType()") 123 | })() 124 | 125 | // Fail in dynamic event handler 126 | Next(Dynamic{ID: 42}) 127 | assert.Equal(t, "OnType()", (<-errors).Error()) 128 | } 129 | 130 | func TestOnEvery(t *testing.T) { 131 | events := make(chan MyEvent2) 132 | defer OnEvery(func(now time.Time, elapsed time.Duration) error { 133 | events <- MyEvent2{} 134 | return nil 135 | }, 20*time.Millisecond)() 136 | 137 | // Emit the event 138 | <-events 139 | <-events 140 | <-events 141 | } 142 | 143 | func TestEveryCancel(t *testing.T) { 144 | var count atomic.Int32 145 | defer On(func(ev MyEvent2, now time.Time, elapsed time.Duration) error { 146 | // Only count events that belong to this test 147 | if ev.Text == "TestEveryCancel" { 148 | count.Add(1) 149 | } 150 | return nil 151 | })() 152 | 153 | // Start recurring event 154 | cancel := Every(MyEvent2{Text: "TestEveryCancel"}, 20*time.Millisecond) 155 | cancel() 156 | 157 | // Wait a bit to ensure no more events come 158 | time.Sleep(100 * time.Millisecond) 159 | 160 | assert.LessOrEqual(t, count.Load(), int32(1), "No events should have been emitted after cancel") 161 | } 162 | 163 | // ------------------------------------- Test Events ------------------------------------- 164 | 165 | const ( 166 | TypeEvent1 = 0x1 167 | TypeEvent2 = 0x2 168 | ) 169 | 170 | type MyEvent1 struct { 171 | Number int 172 | } 173 | 174 | func (t MyEvent1) Type() uint32 { return TypeEvent1 } 175 | 176 | type MyEvent2 struct { 177 | Text string 178 | } 179 | 180 | func (t MyEvent2) Type() uint32 { return TypeEvent2 } 181 | 182 | type Dynamic struct { 183 | ID int 184 | } 185 | 186 | func (t Dynamic) Type() uint32 { 187 | return uint32(t.ID) 188 | } 189 | -------------------------------------------------------------------------------- /example/event/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/kelindar/timeline/emit" 8 | ) 9 | 10 | // Say event type for dialogue 11 | type Say struct { 12 | Character string 13 | Line string 14 | } 15 | 16 | // Type returns the type of the Say event 17 | func (Say) Type() uint32 { return 0x1 } 18 | 19 | // Do event type for actions (with closures) 20 | type Do struct { 21 | Action func() 22 | } 23 | 24 | // Type returns the type of the Do event 25 | func (Do) Type() uint32 { return 0x2 } 26 | 27 | func main() { 28 | // Subscribe to Say events for dialogue 29 | sayCancel := emit.On(func(ev Say, now time.Time, elapsed time.Duration) error { 30 | fmt.Printf("[%02d.%03d] %s: %s\n", 31 | now.Second(), now.UnixMilli()%1000, 32 | ev.Character, ev.Line, 33 | ) 34 | return nil 35 | }) 36 | defer sayCancel() 37 | 38 | // Subscribe to Do events for actions 39 | doCancel := emit.On(func(ev Do, now time.Time, elapsed time.Duration) error { 40 | ev.Action() // Execute the closure 41 | return nil 42 | }) 43 | defer doCancel() 44 | 45 | // Schedule the dialogue using emit timing functions 46 | scheduleDialogue() 47 | 48 | // Let the dialogue play out over about 10 seconds 49 | time.Sleep(10 * time.Second) 50 | } 51 | 52 | func scheduleDialogue() { 53 | // Demonstrate emit.Next() - immediate events 54 | emit.Next(Say{Character: "🎵 Narrator", Line: "Journey begins..."}) 55 | 56 | // Demonstrate emit.After() - scheduled delays 57 | emit.After(Say{Character: "🐴 Donkey", Line: "Are we there yet?"}, 500*time.Millisecond) 58 | emit.After(Say{Character: "👹 Shrek", Line: "No."}, 1*time.Second) 59 | 60 | // Demonstrate emit.EveryAfter() with cancellation - recurring annoyance! 61 | donkeyNagging := emit.EveryAfter(Say{Character: "🐴 Donkey", Line: "Are we there YET?"}, 900*time.Millisecond, 2*time.Second) 62 | 63 | // Responses to the recurring annoyance (offset to avoid collisions) 64 | emit.After(Say{Character: "👹 Shrek", Line: "NO!"}, 2500*time.Millisecond) 65 | emit.After(Say{Character: "👸 Fiona", Line: "NOT YET!"}, 3200*time.Millisecond) 66 | emit.After(Say{Character: "👹 Shrek", Line: "STOP ASKING!"}, 4100*time.Millisecond) 67 | 68 | // Stop Donkey's nagging when Shrek gets really mad - use Do event with closure 69 | emit.After(Do{Action: func() { 70 | donkeyNagging() // Cancel the recurring event 71 | fmt.Println("🎭 [Donkey stops nagging]") 72 | }}, 5*time.Second) 73 | 74 | // Dramatic pause with background sounds 75 | emit.After(Say{Character: "🎵 Narrator", Line: "[awkward silence]"}, 5500*time.Millisecond) 76 | lipPopping := emit.EveryAfter(Say{Character: "🎵 Narrator", Line: "*pop*"}, 600*time.Millisecond, 6*time.Second) 77 | 78 | // Final explosion 79 | emit.After(Say{Character: "👹 Shrek", Line: "THAT'S IT!!!"}, 8*time.Second) 80 | 81 | // Stop the lip popping when Fiona announces arrival - use Do event with closure 82 | emit.After(Do{Action: func() { 83 | lipPopping() // Cancel the lip popping 84 | fmt.Println("🎭 [Background sounds stop]") 85 | }}, 8500*time.Millisecond) 86 | 87 | emit.After(Say{Character: "👸 Fiona", Line: "We're here!"}, 8500*time.Millisecond) 88 | emit.After(Say{Character: "🐴 Donkey", Line: "Finally! 🎉"}, 9*time.Second) 89 | } 90 | -------------------------------------------------------------------------------- /example/timeline/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/kelindar/timeline" 9 | ) 10 | 11 | func main() { 12 | // Initialize the scheduler and start the internal clock 13 | scheduler := timeline.New() 14 | cancel := scheduler.Start(context.Background()) 15 | defer cancel() // Call this to stop the scheduler's internal clock 16 | 17 | // Define a task 18 | task := func(now time.Time, elapsed time.Duration) bool { 19 | fmt.Printf("Task executed at %02d.%03d, elapsed=%v\n", 20 | now.Second(), now.UnixMilli()%1000, elapsed) 21 | return true // return true to keep the task scheduled 22 | } 23 | 24 | // Schedule the task to run immediately 25 | scheduler.Run(task) 26 | 27 | // Schedule the task to run every second 28 | scheduler.RunEvery(task, 1*time.Second) 29 | 30 | // Schedule the task to run after 5 seconds 31 | scheduler.RunAfter(task, 5*time.Second) 32 | 33 | // Let the scheduler run for 10 seconds 34 | time.Sleep(10 * time.Second) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelindar/timeline 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/kelindar/event v1.5.2 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kelindar/event v1.5.2 h1:qtgssZqMh/QQMCIxlbx4wU3DoMHOrJXKdiZhphJ4YbY= 4 | github.com/kelindar/event v1.5.2/go.mod h1:UxWPQjWK8u0o9Z3ponm2mgREimM95hm26/M9z8F488Q= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /timeline.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root 3 | 4 | package timeline 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | const ( 14 | resolution = 10 * time.Millisecond 15 | numBuckets = int(1 * time.Second / resolution) 16 | maxJobs = 1e5 // ~ 10M ev/s 17 | ) 18 | 19 | // Task defines a scheduled function. 'now' is the execution time, and 'elapsed' 20 | // indicates the time since the last schedule or execution. The return value of 21 | // the function is a boolean. If the task returns 'true', it indicates that the 22 | // task should continue to be scheduled for future execution based on its 23 | // interval. Returning 'false' implies that the task should not be executed again. 24 | type Task = func(now time.Time, elapsed time.Duration) bool 25 | 26 | // job represents a scheduled task. 27 | type job struct { 28 | Task 29 | RunAt tick // When the task should run 30 | Since span // Elapsed ticks between scheduled time and starting time 31 | Every span // (optional) In ticks, how often the task should run (0 = once) 32 | } 33 | 34 | // bucket represents a bucket for a particular window of the second. 35 | type bucket struct { 36 | mu sync.Mutex 37 | queue []job 38 | } 39 | 40 | // Scheduler manages and executes scheduled tasks. 41 | type Scheduler struct { 42 | next atomic.Int64 // next tick 43 | buckets []*bucket // Buckets for scheduling jobs 44 | jobs atomic.Int32 // Number of jobs currently scheduled 45 | } 46 | 47 | // New initializes and returns a new Scheduler. 48 | func New() *Scheduler { 49 | s := &Scheduler{ 50 | buckets: make([]*bucket, numBuckets), 51 | } 52 | 53 | for i := 0; i < numBuckets; i++ { 54 | s.buckets[i] = &bucket{ 55 | queue: make([]job, 0, 64), 56 | } 57 | } 58 | 59 | return s 60 | } 61 | 62 | // Run schedules a task for the next tick. 63 | func (s *Scheduler) Run(task Task) { 64 | s.schedule(task, s.now(), 0) 65 | } 66 | 67 | // RunAt schedules a task for a specific 'at' time. 68 | func (s *Scheduler) RunAt(task Task, at time.Time) { 69 | s.schedule(task, tickOf(at), 0) 70 | } 71 | 72 | // RunAfter schedules a task to run after a 'delay'. 73 | func (s *Scheduler) RunAfter(task Task, delay time.Duration) { 74 | s.schedule(task, s.after(delay), 0) 75 | } 76 | 77 | // RunEvery schedules a task to run at 'interval' intervals, starting at the next boundary tick. 78 | func (s *Scheduler) RunEvery(task Task, interval time.Duration) { 79 | s.schedule(task, s.alignedAt(interval), durationOf(interval)) 80 | } 81 | 82 | // RunEveryAt schedules a task to run at 'interval' intervals, starting at 'startTime'. 83 | func (s *Scheduler) RunEveryAt(task Task, interval time.Duration, startTime time.Time) { 84 | s.schedule(task, tickOf(startTime), durationOf(interval)) 85 | } 86 | 87 | // RunEveryAfter schedules a task to run at 'interval' intervals after a 'delay'. 88 | func (s *Scheduler) RunEveryAfter(task Task, interval, delay time.Duration) { 89 | s.schedule(task, s.after(delay), durationOf(interval)) 90 | } 91 | 92 | // schedule schedules an event to be processed at a given time. 93 | func (s *Scheduler) schedule(event Task, when tick, repeat span) { 94 | for count := s.jobs.Add(1); count >= maxJobs; count = s.jobs.Load() { 95 | time.Sleep(500 * time.Microsecond) 96 | } 97 | 98 | s.enqueueJob(job{ 99 | Task: event, 100 | RunAt: when, 101 | Since: span(when - s.now()), 102 | Every: repeat, 103 | }) 104 | } 105 | 106 | // enqueueJob adds a job to the queue. If the queue is full, it will wait briefly. 107 | func (s *Scheduler) enqueueJob(job job) { 108 | bucket := s.bucketOf(job.RunAt) 109 | bucket.mu.Lock() 110 | bucket.queue = append(bucket.queue, job) 111 | bucket.mu.Unlock() 112 | } 113 | 114 | // Seek advances the scheduler to a given time. 115 | func (s *Scheduler) Seek(t time.Time) { 116 | s.next.Store(int64(tickOf(t))) 117 | } 118 | 119 | // Tick processes tasks for the current time and advances the internal clock. 120 | func (s *Scheduler) Tick() time.Time { 121 | tickNow := tick(s.next.Add(1) - 1) 122 | timeNow := tickNow.Time() 123 | bucket := s.bucketOf(tickNow) 124 | offset := 0 125 | 126 | bucket.mu.Lock() 127 | defer bucket.mu.Unlock() 128 | 129 | for i, task := range bucket.queue { 130 | if task.RunAt > tickNow { // scheduled for later 131 | bucket.queue[offset] = bucket.queue[i] 132 | offset++ 133 | continue 134 | } 135 | 136 | // Process the task 137 | repeat := task.Task(timeNow, task.Since.Duration()) 138 | 139 | // If the task is recurrent, determine how to reschedule it 140 | if repeat && task.Every != 0 { 141 | nextTick := tickNow + tick(task.Every) 142 | switch { 143 | case s.bucketOf(nextTick) == s.bucketOf(tickNow): 144 | task.Since = span(nextTick - tickNow) 145 | task.RunAt = nextTick 146 | bucket.queue[offset] = task 147 | offset++ 148 | default: // different bucket 149 | s.enqueueJob(job{ 150 | Task: task.Task, 151 | RunAt: nextTick, 152 | Since: task.Every, 153 | Every: task.Every, 154 | }) 155 | } 156 | } else { 157 | s.jobs.Add(-1) 158 | } 159 | } 160 | 161 | // Truncate the current bucket to remove processed events 162 | bucket.queue = bucket.queue[:offset] 163 | return tickNow.Time() 164 | } 165 | 166 | // bucketOf returns the bucket index for a given tick. 167 | func (s *Scheduler) bucketOf(when tick) *bucket { 168 | idx := int(when) % numBuckets 169 | return s.buckets[idx] 170 | } 171 | 172 | // ----------------------------------------- Clock ----------------------------------------- 173 | 174 | // now returns the current tick. 175 | func (s *Scheduler) now() tick { 176 | return tick(s.next.Load()) 177 | } 178 | 179 | // after calculates the next tick after the specified duration. 180 | func (s *Scheduler) after(dt time.Duration) tick { 181 | return s.now() + tick(durationOf(dt)) 182 | } 183 | 184 | // alignedAt calculates the next tick boundary based on the current tick and the desired interval. 185 | func (s *Scheduler) alignedAt(i time.Duration) tick { 186 | current := s.now() 187 | interval := tick(durationOf(i)) 188 | return current + interval - current%interval 189 | } 190 | 191 | // Start begins the scheduler's internal clock, aligning with the specified 192 | // 'interval'. It returns a cancel function to stop the clock. 193 | func (s *Scheduler) Start(ctx context.Context) context.CancelFunc { 194 | interval := resolution 195 | ctx, cancel := context.WithCancel(ctx) 196 | 197 | // Align the scheduler's internal clock with the nearest resolution boundary 198 | now := time.Now() 199 | next := now.Truncate(interval).Add(interval) 200 | s.Seek(next) 201 | 202 | // Wait until the next resolution boundary 203 | time.Sleep(next.Sub(now)) 204 | 205 | // Start the ticker 206 | ticker := time.NewTicker(interval) 207 | s.Tick() 208 | go func() { 209 | for { 210 | select { 211 | case <-ticker.C: 212 | s.Tick() 213 | case <-ctx.Done(): 214 | ticker.Stop() 215 | return 216 | } 217 | } 218 | }() 219 | 220 | return cancel 221 | } 222 | 223 | // ----------------------------------------- Time (in ticks) ----------------------------------------- 224 | 225 | // tick represents a point in time, rounded up to the resolution of the clock. 226 | type tick int64 227 | 228 | // Time converts the tick to a timestamp. 229 | func (t tick) Time() time.Time { 230 | return time.Unix(0, int64(t)*int64(resolution)) 231 | } 232 | 233 | // tickOf returns the time rounded up to the resolution of the clock. 234 | func tickOf(t time.Time) tick { 235 | return tick(t.UnixNano() / int64(resolution)) 236 | } 237 | 238 | // ----------------------------------------- Duration (in ticks) ----------------------------------------- 239 | 240 | // span represents a time span (duration) in ticks 241 | type span uint32 242 | 243 | // Duration converts the span to a duration. 244 | func (s span) Duration() time.Duration { 245 | return time.Duration(s) * resolution 246 | } 247 | 248 | // durationOf computes a duration in terms of ticks. 249 | func durationOf(t time.Duration) span { 250 | return span(t / resolution) 251 | } 252 | -------------------------------------------------------------------------------- /timeline_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root 3 | 4 | package timeline 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "sync" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | "unsafe" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | var counter atomic.Uint64 19 | 20 | /* 21 | cpu: 13th Gen Intel(R) Core(TM) i7-13700K 22 | BenchmarkRun/next-24 11874015 100.4 ns/op 11.79 million/op 117 B/op 0 allocs/op 23 | BenchmarkRun/after-24 10000 471016 ns/op 0.9101 million/op 1469 B/op 0 allocs/op 24 | */ 25 | func BenchmarkRun(b *testing.B) { 26 | work := func(time.Time, time.Duration) bool { 27 | counter.Add(1) 28 | return true 29 | } 30 | 31 | b.Run("next", func(b *testing.B) { 32 | counter.Store(0) 33 | s := New() 34 | s.Start(context.Background()) 35 | b.ReportAllocs() 36 | b.ResetTimer() 37 | 38 | for n := 0; n < b.N; n++ { 39 | s.Run(work) 40 | } 41 | 42 | b.ReportMetric(float64(counter.Load())/1000000, "million/op") 43 | }) 44 | 45 | b.Run("after", func(b *testing.B) { 46 | counter.Store(0) 47 | s := New() 48 | s.Start(context.Background()) 49 | b.ReportAllocs() 50 | b.ResetTimer() 51 | 52 | for n := 0; n < b.N; n++ { 53 | for i := 0; i < 100; i++ { 54 | s.RunAfter(work, time.Duration(10*i)*time.Millisecond) 55 | } 56 | } 57 | 58 | b.ReportMetric(float64(counter.Load())/1000000, "million/op") 59 | }) 60 | } 61 | 62 | func TestRunAt(t *testing.T) { 63 | now := time.Unix(0, 0) 64 | log := make(Log, 0, 8) 65 | 66 | s := newScheduler(now) 67 | s.RunAt(log.Log("Next 1"), now) 68 | s.RunAt(log.Log("Next 2"), now.Add(5*time.Millisecond)) 69 | s.RunAt(log.Log("Future 1"), now.Add(495*time.Millisecond)) 70 | s.RunAt(log.Log("Future 2"), now.Add(1600*time.Millisecond)) 71 | 72 | for i := 0; i < 200; i++ { 73 | s.Tick() 74 | } 75 | 76 | assert.Equal(t, Log{ 77 | "Next 1", 78 | "Next 2", 79 | "Future 1", 80 | "Future 2", 81 | }, log) 82 | } 83 | 84 | func TestRunAfter(t *testing.T) { 85 | now := time.Unix(0, 0) 86 | log := make(Log, 0, 8) 87 | 88 | s := newScheduler(now) 89 | s.RunAfter(log.Log("Next 1"), 0) 90 | s.RunAfter(log.Log("Next 2"), 5*time.Millisecond) 91 | s.RunAfter(log.Log("Future 1"), 495*time.Millisecond) 92 | s.RunAfter(log.Log("Future 2"), 1600*time.Millisecond) 93 | 94 | for i := 0; i < 200; i++ { 95 | s.Tick() 96 | } 97 | 98 | assert.Equal(t, Log{ 99 | "Next 1", 100 | "Next 2", 101 | "Future 1", 102 | "Future 2", 103 | }, log) 104 | } 105 | 106 | func TestRunEveryAt(t *testing.T) { 107 | now := time.Unix(0, 0) 108 | var count Counter 109 | 110 | s := newScheduler(now) 111 | s.RunEveryAt(count.Inc(), 10*time.Millisecond, now) 112 | s.RunEveryAt(count.Inc(), 30*time.Millisecond, now.Add(50*time.Millisecond)) 113 | 114 | for i := 0; i < 10; i++ { 115 | s.Tick() 116 | } 117 | 118 | assert.Equal(t, 12, count.Value()) 119 | } 120 | 121 | func TestRunEveryAfter(t *testing.T) { 122 | now := time.Unix(0, 0) 123 | var count Counter 124 | 125 | s := newScheduler(now) 126 | s.RunEveryAfter(count.Inc(), 10*time.Millisecond, 0) 127 | s.RunEveryAfter(count.Inc(), 30*time.Millisecond, 50*time.Millisecond) 128 | 129 | for i := 0; i < 10; i++ { 130 | s.Tick() 131 | } 132 | 133 | assert.Equal(t, 12, count.Value()) 134 | } 135 | 136 | func TestRunEvery10ms(t *testing.T) { 137 | now := time.Unix(0, 0) 138 | var count Counter 139 | 140 | s := newScheduler(now) 141 | s.RunEvery(count.Inc(), 10*time.Millisecond) 142 | 143 | for i := 0; i < 10; i++ { 144 | s.Tick() 145 | } 146 | 147 | assert.Equal(t, 9, count.Value()) 148 | } 149 | 150 | func TestRunEvery1s(t *testing.T) { 151 | now := time.Unix(0, 0) 152 | var count Counter 153 | 154 | s := newScheduler(now) 155 | s.RunEvery(count.Inc(), 1*time.Second) 156 | 157 | for i := 0; i < 510; i++ { 158 | s.Tick() 159 | } 160 | 161 | assert.Equal(t, 5, count.Value()) 162 | } 163 | 164 | func TestRun(t *testing.T) { 165 | now := time.Unix(0, 0) 166 | var count Counter 167 | 168 | s := newScheduler(now) 169 | s.Run(count.Inc()) 170 | s.Run(count.Inc()) 171 | 172 | for i := 0; i < 10; i++ { 173 | s.Tick() 174 | } 175 | 176 | assert.Equal(t, 2, count.Value()) 177 | } 178 | 179 | func TestElapsed(t *testing.T) { 180 | s := New() 181 | 182 | var wg sync.WaitGroup 183 | wg.Add(3) 184 | s.RunEvery(func(now time.Time, elapsed time.Duration) bool { 185 | fmt.Printf("Tick at %02d.%03d, elapsed=%v\n", 186 | now.Second(), now.UnixMilli()%1000, elapsed) 187 | assert.Equal(t, 10*time.Millisecond, elapsed) 188 | wg.Done() 189 | return true 190 | }, 10*time.Millisecond) 191 | 192 | s.Tick() 193 | s.Tick() 194 | s.Tick() 195 | s.Tick() 196 | wg.Wait() 197 | } 198 | 199 | func TestTickOf(t *testing.T) { 200 | tc := map[tick]time.Duration{ 201 | 0: 0, 202 | 1: 10 * time.Millisecond, 203 | 2: 20 * time.Millisecond, 204 | 10: 100 * time.Millisecond, 205 | 100: time.Second, 206 | 101: time.Second + 10*time.Millisecond, 207 | 360000: time.Hour, 208 | } 209 | 210 | for expect, duration := range tc { 211 | assert.Equal(t, expect, tickOf(time.Unix(0, int64(duration)))) 212 | } 213 | } 214 | 215 | func TestStart(t *testing.T) { 216 | s := New() 217 | defer s.Start(context.Background())() 218 | 219 | var count Counter 220 | s.RunAfter(count.Inc(), 30*time.Millisecond) 221 | s.Run(count.Inc()) 222 | s.Run(count.Inc()) 223 | 224 | time.Sleep(100 * time.Millisecond) 225 | assert.Equal(t, 3, count.Value()) 226 | } 227 | 228 | func TestJobSize(t *testing.T) { 229 | size := unsafe.Sizeof(job{}) 230 | assert.Equal(t, 24, int(size)) 231 | } 232 | 233 | // ----------------------------------------- Log ----------------------------------------- 234 | 235 | // Log is a simple task that appends a string to a slice. 236 | type Log []string 237 | 238 | // Log returns a task that appends a string to the log. 239 | func (l *Log) Log(s string) Task { 240 | return func(time.Time, time.Duration) bool { 241 | *l = append(*l, s) 242 | return true 243 | } 244 | } 245 | 246 | // ----------------------------------------- Counter ----------------------------------------- 247 | 248 | type Counter int64 249 | 250 | // Value returns the current value of the counter. 251 | func (c *Counter) Value() int { 252 | return int(atomic.LoadInt64((*int64)(c))) 253 | } 254 | 255 | // Inc returns a task that increments the counter. 256 | func (c *Counter) Inc() Task { 257 | return func(time.Time, time.Duration) bool { 258 | atomic.AddInt64((*int64)(c), 1) 259 | return true 260 | } 261 | } 262 | 263 | // ----------------------------------------- Scheduler ----------------------------------------- 264 | 265 | func newScheduler(now time.Time) *Scheduler { 266 | s := New() 267 | s.Seek(now) 268 | return s 269 | } 270 | --------------------------------------------------------------------------------