perf(core): make the frame fallback timer once and reset it per frame
armFrame built a time.Timer and two closures on every animation frame (about 4 allocations). Core keeps one ports.Timer, made on the first arm and Stop+Reset afterwards; frameC is nil while it is not armed. Mock clocks in tests accept Reset; the multi-output timer test counts resets instead of comparing channels.
- commit
- 7d23734194b9d3d855ad4e23f06394a26950847e
- author
- brice · · 2026-10-05 01:53 UTC
- parent
- 70589ae
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +53 -21
M internal/core/capindicator_test.go +7 -1
| @@ -12,10 +12,11 @@ import ( | |||
| 12 | 12 | ||
| 13 | 13 | // indicatorClock is a clock the test moves; its timers never fire by | |
| 14 | 14 | // themselves: the test calls captureFlashTick, and the durations asked for | |
| 15 | - | // are recorded. | |
| 15 | + | // are recorded: timers holds the NewTimer durations, resets the Reset ones. | |
| 16 | 16 | type indicatorClock struct { | |
| 17 | 17 | now time.Time | |
| 18 | 18 | timers []time.Duration | |
| 19 | + | resets []time.Duration | |
| 19 | 20 | } | |
| 20 | 21 | ||
| 21 | 22 | func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | |
| @@ -28,6 +29,10 @@ func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | |||
| 28 | 29 | timer := portsmocks.NewMockTimer(t) | |
| 29 | 30 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| 30 | 31 | timer.EXPECT().Stop().Return(true).Maybe() | |
| 32 | + | timer.EXPECT().Reset(mock.Anything).RunAndReturn(func(d time.Duration) bool { | |
| 33 | + | ic.resets = append(ic.resets, d) | |
| 34 | + | return false | |
| 35 | + | }).Maybe() | |
| 31 | 36 | return timer | |
| 32 | 37 | }).Maybe() | |
| 33 | 38 | var cfg ports.Config | |
| @@ -193,6 +198,7 @@ func TestCaptureIndicatorHiddenWorkspacePill(t *testing.T) { | |||
| 193 | 198 | timer := portsmocks.NewMockTimer(t) | |
| 194 | 199 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| 195 | 200 | timer.EXPECT().Stop().Return(true).Maybe() | |
| 201 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |
| 196 | 202 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | |
| 197 | 203 | c.ch.Clock = clock | |
| 198 | 204 | c.configures.cw.reset() | |
| @@ -12,10 +12,11 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | // indicatorClock is a clock the test moves; its timers never fire by | 13 | // indicatorClock is a clock the test moves; its timers never fire by | ||
| 14 | // themselves: the test calls captureFlashTick, and the durations asked for | 14 | // themselves: the test calls captureFlashTick, and the durations asked for | ||
| 15 | - | // are recorded. | 15 | + | // are recorded: timers holds the NewTimer durations, resets the Reset ones. |
| 16 | type indicatorClock struct { | 16 | type indicatorClock struct { | ||
| 17 | now time.Time | 17 | now time.Time | ||
| 18 | timers []time.Duration | 18 | timers []time.Duration | ||
| 19 | + | resets []time.Duration | |||
| 19 | } | 20 | } | ||
| 20 | 21 | ||||
| 21 | func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | 22 | func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | ||
| @@ -28,6 +29,10 @@ func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | |||||
| 28 | timer := portsmocks.NewMockTimer(t) | 29 | timer := portsmocks.NewMockTimer(t) | ||
| 29 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | 30 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | ||
| 30 | timer.EXPECT().Stop().Return(true).Maybe() | 31 | timer.EXPECT().Stop().Return(true).Maybe() | ||
| 32 | + | timer.EXPECT().Reset(mock.Anything).RunAndReturn(func(d time.Duration) bool { | |||
| 33 | + | ic.resets = append(ic.resets, d) | |||
| 34 | + | return false | |||
| 35 | + | }).Maybe() | |||
| 31 | return timer | 36 | return timer | ||
| 32 | }).Maybe() | 37 | }).Maybe() | ||
| 33 | var cfg ports.Config | 38 | var cfg ports.Config | ||
| @@ -193,6 +198,7 @@ func TestCaptureIndicatorHiddenWorkspacePill(t *testing.T) { | |||||
| 193 | timer := portsmocks.NewMockTimer(t) | 198 | timer := portsmocks.NewMockTimer(t) | ||
| 194 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | 199 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | ||
| 195 | timer.EXPECT().Stop().Return(true).Maybe() | 200 | timer.EXPECT().Stop().Return(true).Maybe() | ||
| 201 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |||
| 196 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | 202 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | ||
| 197 | c.ch.Clock = clock | 203 | c.ch.Clock = clock | ||
| 198 | c.configures.cw.reset() | 204 | c.configures.cw.reset() | ||
M internal/core/core.go +23 -9
| @@ -128,10 +128,12 @@ type Core struct { | |||
| 128 | 128 | // (ports.UserActivity), sent at most once per ActivityInterval. | |
| 129 | 129 | activity time.Time | |
| 130 | 130 | // swipe is the touchpad swipe in progress. frameC fires when no page | |
| 131 | - | // flip came in time to move a running slide (frameStop stops it). | |
| 132 | - | swipe *swipeGesture | |
| 133 | - | frameC <-chan time.Time | |
| 134 | - | frameStop func() bool | |
| 131 | + | // flip came in time to move a running slide; nil when not armed. It is | |
| 132 | + | // the channel of frameTimer, made once and reset per frame: an | |
| 133 | + | // animation frame allocates no timer. | |
| 134 | + | swipe *swipeGesture | |
| 135 | + | frameC <-chan time.Time | |
| 136 | + | frameTimer ports.Timer | |
| 135 | 137 | // shots is the snapshot of what the screens show before an action | |
| 136 | 138 | // (transition.go), reused by every action. | |
| 137 | 139 | shots []viewShot | |
| @@ -360,14 +362,26 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||
| 360 | 362 | ||
| 361 | 363 | // newTimer makes a timer on clock, or the system timer when clock is nil. | |
| 362 | 364 | func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) { | |
| 365 | + | t := newPortTimer(clock, d) | |
| 366 | + | return t.C(), t.Stop | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // newPortTimer makes a ports.Timer on clock, or on the system clock when | |
| 370 | + | // clock is nil. | |
| 371 | + | func newPortTimer(clock ports.Clock, d time.Duration) ports.Timer { | |
| 363 | 372 | if clock != nil { | |
| 364 | - | t := clock.NewTimer(d) | |
| 365 | - | return t.C(), t.Stop | |
| 373 | + | return clock.NewTimer(d) | |
| 366 | 374 | } | |
| 367 | - | t := time.NewTimer(d) | |
| 368 | - | return t.C, t.Stop | |
| 375 | + | return sysTimer{time.NewTimer(d)} | |
| 369 | 376 | } | |
| 370 | 377 | ||
| 378 | + | // sysTimer is a time.Timer as a ports.Timer. | |
| 379 | + | type sysTimer struct{ t *time.Timer } | |
| 380 | + | ||
| 381 | + | func (t sysTimer) C() <-chan time.Time { return t.t.C } | |
| 382 | + | func (t sysTimer) Stop() bool { return t.t.Stop() } | |
| 383 | + | func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } | |
| 384 | + | ||
| 371 | 385 | func (c *Core) now() time.Time { | |
| 372 | 386 | if c.ch.Clock != nil { | |
| 373 | 387 | return c.ch.Clock.Now() | |
| @@ -1236,7 +1250,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1236 | 1250 | if c.securityCheckpoint(ctx) != nil { | |
| 1237 | 1251 | return nil | |
| 1238 | 1252 | } | |
| 1239 | - | c.frameC, c.frameStop = nil, nil | |
| 1253 | + | c.frameC = nil | |
| 1240 | 1254 | if c.step(ctx, nil) != nil { | |
| 1241 | 1255 | return nil | |
| 1242 | 1256 | } | |
| @@ -128,10 +128,12 @@ type Core struct { | |||||
| 128 | // (ports.UserActivity), sent at most once per ActivityInterval. | 128 | // (ports.UserActivity), sent at most once per ActivityInterval. | ||
| 129 | activity time.Time | 129 | activity time.Time | ||
| 130 | // swipe is the touchpad swipe in progress. frameC fires when no page | 130 | // swipe is the touchpad swipe in progress. frameC fires when no page | ||
| 131 | - | // flip came in time to move a running slide (frameStop stops it). | 131 | + | // flip came in time to move a running slide; nil when not armed. It is |
| 132 | - | swipe *swipeGesture | 132 | + | // the channel of frameTimer, made once and reset per frame: an |
| 133 | - | frameC <-chan time.Time | 133 | + | // animation frame allocates no timer. |
| 134 | - | frameStop func() bool | 134 | + | swipe *swipeGesture |
| 135 | + | frameC <-chan time.Time | |||
| 136 | + | frameTimer ports.Timer | |||
| 135 | // shots is the snapshot of what the screens show before an action | 137 | // shots is the snapshot of what the screens show before an action | ||
| 136 | // (transition.go), reused by every action. | 138 | // (transition.go), reused by every action. | ||
| 137 | shots []viewShot | 139 | shots []viewShot | ||
| @@ -360,14 +362,26 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||||
| 360 | 362 | ||||
| 361 | // newTimer makes a timer on clock, or the system timer when clock is nil. | 363 | // newTimer makes a timer on clock, or the system timer when clock is nil. | ||
| 362 | func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) { | 364 | func newTimer(clock ports.Clock, d time.Duration) (<-chan time.Time, func() bool) { | ||
| 365 | + | t := newPortTimer(clock, d) | |||
| 366 | + | return t.C(), t.Stop | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | // newPortTimer makes a ports.Timer on clock, or on the system clock when | |||
| 370 | + | // clock is nil. | |||
| 371 | + | func newPortTimer(clock ports.Clock, d time.Duration) ports.Timer { | |||
| 363 | if clock != nil { | 372 | if clock != nil { | ||
| 364 | - | t := clock.NewTimer(d) | 373 | + | return clock.NewTimer(d) |
| 365 | - | return t.C(), t.Stop | |||
| 366 | } | 374 | } | ||
| 367 | - | t := time.NewTimer(d) | 375 | + | return sysTimer{time.NewTimer(d)} |
| 368 | - | return t.C, t.Stop | |||
| 369 | } | 376 | } | ||
| 370 | 377 | ||||
| 378 | + | // sysTimer is a time.Timer as a ports.Timer. | |||
| 379 | + | type sysTimer struct{ t *time.Timer } | |||
| 380 | + | ||||
| 381 | + | func (t sysTimer) C() <-chan time.Time { return t.t.C } | |||
| 382 | + | func (t sysTimer) Stop() bool { return t.t.Stop() } | |||
| 383 | + | func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } | |||
| 384 | + | ||||
| 371 | func (c *Core) now() time.Time { | 385 | func (c *Core) now() time.Time { | ||
| 372 | if c.ch.Clock != nil { | 386 | if c.ch.Clock != nil { | ||
| 373 | return c.ch.Clock.Now() | 387 | return c.ch.Clock.Now() | ||
| @@ -1236,7 +1250,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1236 | if c.securityCheckpoint(ctx) != nil { | 1250 | if c.securityCheckpoint(ctx) != nil { | ||
| 1237 | return nil | 1251 | return nil | ||
| 1238 | } | 1252 | } | ||
| 1239 | - | c.frameC, c.frameStop = nil, nil | 1253 | + | c.frameC = nil |
| 1240 | if c.step(ctx, nil) != nil { | 1254 | if c.step(ctx, nil) != nil { | ||
| 1241 | return nil | 1255 | return nil | ||
| 1242 | } | 1256 | } | ||
M internal/core/gesture.go +14 -6
| @@ -465,15 +465,23 @@ func (c *Core) frameFallback() time.Duration { | |||
| 465 | 465 | return 2 * time.Second * 1000 / time.Duration(refresh) | |
| 466 | 466 | } | |
| 467 | 467 | ||
| 468 | - | // armFrame starts the fallback timer of a running slide. | |
| 468 | + | // armFrame starts the fallback timer of a running slide. The timer is made | |
| 469 | + | // on the first arm and reset afterwards (Go 1.23 timers deliver nothing | |
| 470 | + | // stale after a Stop or Reset), so a frame costs no allocation. | |
| 469 | 471 | func (c *Core) armFrame() { | |
| 470 | - | c.stopFrame() | |
| 471 | - | c.frameC, c.frameStop = newTimer(c.ch.Clock, c.frameFallback()) | |
| 472 | + | d := c.frameFallback() | |
| 473 | + | if c.frameTimer == nil { | |
| 474 | + | c.frameTimer = newPortTimer(c.ch.Clock, d) | |
| 475 | + | } else { | |
| 476 | + | c.frameTimer.Stop() | |
| 477 | + | c.frameTimer.Reset(d) | |
| 478 | + | } | |
| 479 | + | c.frameC = c.frameTimer.C() | |
| 472 | 480 | } | |
| 473 | 481 | ||
| 474 | 482 | func (c *Core) stopFrame() { | |
| 475 | - | if c.frameStop != nil { | |
| 476 | - | c.frameStop() | |
| 483 | + | if c.frameC != nil { | |
| 484 | + | c.frameTimer.Stop() | |
| 477 | 485 | } | |
| 478 | - | c.frameC, c.frameStop = nil, nil | |
| 486 | + | c.frameC = nil | |
| 479 | 487 | } | |
| @@ -465,15 +465,23 @@ func (c *Core) frameFallback() time.Duration { | |||||
| 465 | return 2 * time.Second * 1000 / time.Duration(refresh) | 465 | return 2 * time.Second * 1000 / time.Duration(refresh) | ||
| 466 | } | 466 | } | ||
| 467 | 467 | ||||
| 468 | - | // armFrame starts the fallback timer of a running slide. | 468 | + | // armFrame starts the fallback timer of a running slide. The timer is made |
| 469 | + | // on the first arm and reset afterwards (Go 1.23 timers deliver nothing | |||
| 470 | + | // stale after a Stop or Reset), so a frame costs no allocation. | |||
| 469 | func (c *Core) armFrame() { | 471 | func (c *Core) armFrame() { | ||
| 470 | - | c.stopFrame() | 472 | + | d := c.frameFallback() |
| 471 | - | c.frameC, c.frameStop = newTimer(c.ch.Clock, c.frameFallback()) | 473 | + | if c.frameTimer == nil { |
| 474 | + | c.frameTimer = newPortTimer(c.ch.Clock, d) | |||
| 475 | + | } else { | |||
| 476 | + | c.frameTimer.Stop() | |||
| 477 | + | c.frameTimer.Reset(d) | |||
| 478 | + | } | |||
| 479 | + | c.frameC = c.frameTimer.C() | |||
| 472 | } | 480 | } | ||
| 473 | 481 | ||||
| 474 | func (c *Core) stopFrame() { | 482 | func (c *Core) stopFrame() { | ||
| 475 | - | if c.frameStop != nil { | 483 | + | if c.frameC != nil { |
| 476 | - | c.frameStop() | 484 | + | c.frameTimer.Stop() |
| 477 | } | 485 | } | ||
| 478 | - | c.frameC, c.frameStop = nil, nil | 486 | + | c.frameC = nil |
| 479 | } | 487 | } | ||
M internal/core/persist_test.go +1 -0
| @@ -28,6 +28,7 @@ func steppingClock(t *testing.T) *portsmocks.MockClock { | |||
| 28 | 28 | timer := portsmocks.NewMockTimer(t) | |
| 29 | 29 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| 30 | 30 | timer.EXPECT().Stop().Return(true).Maybe() | |
| 31 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |
| 31 | 32 | return timer | |
| 32 | 33 | }).Maybe() | |
| 33 | 34 | return clock | |
| @@ -28,6 +28,7 @@ func steppingClock(t *testing.T) *portsmocks.MockClock { | |||||
| 28 | timer := portsmocks.NewMockTimer(t) | 28 | timer := portsmocks.NewMockTimer(t) | ||
| 29 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | 29 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | ||
| 30 | timer.EXPECT().Stop().Return(true).Maybe() | 30 | timer.EXPECT().Stop().Return(true).Maybe() | ||
| 31 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |||
| 31 | return timer | 32 | return timer | ||
| 32 | }).Maybe() | 33 | }).Maybe() | ||
| 33 | return clock | 34 | return clock | ||
M internal/core/step_clock_test.go +1 -0
| @@ -33,6 +33,7 @@ func newStepClock(t *testing.T) *stepClock { | |||
| 33 | 33 | timer := portsmocks.NewMockTimer(t) | |
| 34 | 34 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| 35 | 35 | timer.EXPECT().Stop().Return(true).Maybe() | |
| 36 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |
| 36 | 37 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | |
| 37 | 38 | s.clock = clock | |
| 38 | 39 | return s | |
| @@ -33,6 +33,7 @@ func newStepClock(t *testing.T) *stepClock { | |||||
| 33 | timer := portsmocks.NewMockTimer(t) | 33 | timer := portsmocks.NewMockTimer(t) | ||
| 34 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | 34 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | ||
| 35 | timer.EXPECT().Stop().Return(true).Maybe() | 35 | timer.EXPECT().Stop().Return(true).Maybe() | ||
| 36 | + | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |||
| 36 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | 37 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | ||
| 37 | s.clock = clock | 38 | s.clock = clock | ||
| 38 | return s | 39 | return s | ||
M internal/core/transition_chain_internal_test.go +7 -5
| @@ -146,15 +146,17 @@ func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |||
| 146 | 146 | w := sc.mon.Current() | |
| 147 | 147 | w.shift, w.motion = 100, c.spring(viewSpring(100, 0), ic.now) | |
| 148 | 148 | } | |
| 149 | + | // The one frame timer is made on the first arm and reset afterwards: | |
| 150 | + | // a re-arm shows as a Reset on the clock. | |
| 149 | 151 | c.armFrame() | |
| 150 | - | armed := c.frameC | |
| 152 | + | resets := len(ic.resets) | |
| 151 | 153 | ic.now = ic.now.Add(16 * time.Millisecond) | |
| 152 | 154 | if err := c.step(context.Background(), a); err != nil { | |
| 153 | 155 | t.Fatal(err) | |
| 154 | 156 | } | |
| 155 | 157 | <-c.ch.Scenes | |
| 156 | - | if c.frameC == nil || c.frameC != armed { | |
| 157 | - | t.Fatal("a flip of A re-armed or dropped the fallback timer while B animates") | |
| 158 | + | if c.frameC == nil || len(ic.resets) != resets { | |
| 159 | + | t.Fatalf("a flip of A re-armed or dropped the fallback timer while B animates: armed %t, resets %d", c.frameC != nil, len(ic.resets)-resets) | |
| 158 | 160 | } | |
| 159 | 161 | ||
| 160 | 162 | // With nothing else animating the flip's own publish decides. | |
| @@ -163,8 +165,8 @@ func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |||
| 163 | 165 | t.Fatal(err) | |
| 164 | 166 | } | |
| 165 | 167 | <-c.ch.Scenes | |
| 166 | - | if c.frameC == nil || c.frameC == armed { | |
| 167 | - | t.Fatal("A still animates but its timer was not re-armed") | |
| 168 | + | if c.frameC == nil || len(ic.resets) != resets+1 { | |
| 169 | + | t.Fatalf("A still animates but its timer was not re-armed: armed %t, resets %d", c.frameC != nil, len(ic.resets)-resets) | |
| 168 | 170 | } | |
| 169 | 171 | ic.now = ic.now.Add(5 * time.Second) | |
| 170 | 172 | if err := c.step(context.Background(), nil); err != nil { | |
| @@ -146,15 +146,17 @@ func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |||||
| 146 | w := sc.mon.Current() | 146 | w := sc.mon.Current() | ||
| 147 | w.shift, w.motion = 100, c.spring(viewSpring(100, 0), ic.now) | 147 | w.shift, w.motion = 100, c.spring(viewSpring(100, 0), ic.now) | ||
| 148 | } | 148 | } | ||
| 149 | + | // The one frame timer is made on the first arm and reset afterwards: | |||
| 150 | + | // a re-arm shows as a Reset on the clock. | |||
| 149 | c.armFrame() | 151 | c.armFrame() | ||
| 150 | - | armed := c.frameC | 152 | + | resets := len(ic.resets) |
| 151 | ic.now = ic.now.Add(16 * time.Millisecond) | 153 | ic.now = ic.now.Add(16 * time.Millisecond) | ||
| 152 | if err := c.step(context.Background(), a); err != nil { | 154 | if err := c.step(context.Background(), a); err != nil { | ||
| 153 | t.Fatal(err) | 155 | t.Fatal(err) | ||
| 154 | } | 156 | } | ||
| 155 | <-c.ch.Scenes | 157 | <-c.ch.Scenes | ||
| 156 | - | if c.frameC == nil || c.frameC != armed { | 158 | + | if c.frameC == nil || len(ic.resets) != resets { |
| 157 | - | t.Fatal("a flip of A re-armed or dropped the fallback timer while B animates") | 159 | + | t.Fatalf("a flip of A re-armed or dropped the fallback timer while B animates: armed %t, resets %d", c.frameC != nil, len(ic.resets)-resets) |
| 158 | } | 160 | } | ||
| 159 | 161 | ||||
| 160 | // With nothing else animating the flip's own publish decides. | 162 | // With nothing else animating the flip's own publish decides. | ||
| @@ -163,8 +165,8 @@ func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |||||
| 163 | t.Fatal(err) | 165 | t.Fatal(err) | ||
| 164 | } | 166 | } | ||
| 165 | <-c.ch.Scenes | 167 | <-c.ch.Scenes | ||
| 166 | - | if c.frameC == nil || c.frameC == armed { | 168 | + | if c.frameC == nil || len(ic.resets) != resets+1 { |
| 167 | - | t.Fatal("A still animates but its timer was not re-armed") | 169 | + | t.Fatalf("A still animates but its timer was not re-armed: armed %t, resets %d", c.frameC != nil, len(ic.resets)-resets) |
| 168 | } | 170 | } | ||
| 169 | ic.now = ic.now.Add(5 * time.Second) | 171 | ic.now = ic.now.Add(5 * time.Second) | ||
| 170 | if err := c.step(context.Background(), nil); err != nil { | 172 | if err := c.step(context.Background(), nil); err != nil { | ||