From 999c41477c496880384be49c9702996b00a4a266 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 18 Nov 2025 10:07:09 +0100 Subject: [PATCH 1/5] Experiment: track and wrap Timers and run* blocks in the clock control zone. --- app/lib/task/clock_control.dart | 108 +++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/app/lib/task/clock_control.dart b/app/lib/task/clock_control.dart index 4d9212217..37449c785 100644 --- a/app/lib/task/clock_control.dart +++ b/app/lib/task/clock_control.dart @@ -18,9 +18,66 @@ Future withClockControl( final clockCtrl = ClockController._(clock.now, initialTime.difference(now)); return await runZoned( - () => withClock( - Clock(clockCtrl._controlledTime), - () async => await fn(clockCtrl), + () => runZoned( + () => withClock( + Clock(clockCtrl._controlledTime), + () async => await fn(clockCtrl), + ), + zoneSpecification: ZoneSpecification( + run: (self, parent, zone, R Function() fn) { + if (R is Future) { + return parent.run(zone, () => clock.instant(() async => fn())) as R; + } else { + return parent.run(zone, fn); + } + }, + runUnary: (self, parent, zone, R Function(T1) fn, T1 arg1) { + if (R is Future) { + return parent.runUnary( + zone, + (arg1) => clock.instant(() async => fn(arg1)) as R, + arg1, + ); + } else { + return parent.runUnary(zone, fn, arg1); + } + }, + runBinary: + ( + self, + parent, + zone, + R Function(T1, T2) fn, + T1 arg1, + T2 arg2, + ) { + if (R is Future) { + return parent.runBinary( + zone, + (arg1, arg2) => + clock.instant(() async => fn(arg1, arg2)) as R, + arg1, + arg2, + ); + } else { + return parent.runBinary(zone, fn, arg1, arg2); + } + }, + createTimer: (self, parent, zone, duration, f) { + final current = StackTrace.current.toString(); + if (current.contains('ClockController._createTimer') || + current.contains('ClockController._triggerPendingTimers') || + current.contains('ClockController._cancelPendingTimer')) { + return parent.createTimer(zone, duration, f); + } + final clockCtrl = zone[_clockCtrlKey]; + if (clockCtrl is ClockController) { + return clockCtrl._createTimer(duration, f); + } else { + return parent.createTimer(zone, duration, f); + } + }, + ), ), zoneValues: {_clockCtrlKey: clockCtrl}, ); @@ -319,9 +376,10 @@ final class ClockController { final deadline = timeout != null ? clock.fromNowBy(timeout) : null; bool shouldLoop() => - _pendingTimers.isNotEmpty && - (deadline == null || - _pendingTimers.first._elapsesAtInFakeTime.isBefore(deadline)); + _pendingInstants.isNotEmpty || + (_pendingTimers.isNotEmpty && + (deadline == null || + _pendingTimers.first._elapsesAtInFakeTime.isBefore(deadline))); while (shouldLoop()) { if (await condition()) { @@ -342,6 +400,7 @@ final class ClockController { // Trigger all timers that are pending, this cancels any actual timer // and creates a new pending timer. _triggerPendingTimers(); + await _waitForMicroTasks(); } await _waitForMicroTasks(); @@ -391,8 +450,9 @@ final class ClockController { /// a zero duration. Future _elapseTo(DateTime futureTime) async { bool shouldLoop() => - _pendingTimers.isNotEmpty && - _pendingTimers.first._elapsesAtInFakeTime.isBefore(futureTime); + _pendingInstants.isNotEmpty || + (_pendingTimers.isNotEmpty && + _pendingTimers.first._elapsesAtInFakeTime.isBefore(futureTime)); await _waitForMicroTasks(); @@ -455,7 +515,7 @@ final class ClockController { /// Wait for all scheduled microtasks to be done. Future _waitForMicroTasks() async { - await Future.delayed(Duration(microseconds: 0)); + await Future.microtask(() {}); while (_pendingInstants.isNotEmpty) { final f = Future.wait(_pendingInstants); @@ -466,12 +526,12 @@ final class ClockController { // ignore } - await Future.delayed(Duration(microseconds: 0)); + await Future.microtask(() {}); } } } -final class _TravelingTimer { +final class _TravelingTimer implements Timer { /// [ClockController] to which this [_TravelingTimer] belongs. final ClockController _owner; @@ -485,13 +545,23 @@ final class _TravelingTimer { /// Duration for the timer to trigger. final Duration _duration; + final void Function() _triggerFn; + /// Callback to be invoked when this [_TravelingTimer] is triggered. - final void Function(_TravelingTimer timer) _trigger; + void _trigger(_TravelingTimer timer) { + if (isActive) { + _isTriggered = true; + _triggerFn(); + } + } /// [DateTime] when this [_TravelingTimer] is supposed to be triggered, /// measured in [ClockController._controlledTime]. DateTime get _elapsesAtInFakeTime => _createdInControlledTime.add(_duration); + bool _isTriggered = false; + bool _isCancelled = false; + _TravelingTimer({ required ClockController owner, required DateTime createdInControlledTime, @@ -502,7 +572,17 @@ final class _TravelingTimer { _createdInControlledTime = createdInControlledTime, _zone = zone, _duration = duration, - _trigger = ((_) => trigger()); + _triggerFn = trigger; + + @override + void cancel() { + _isCancelled = true; + _owner._cancelPendingTimer(this); + } + + @override + int get tick => throw UnimplementedError(); - void cancel() => _owner._cancelPendingTimer(this); + @override + bool get isActive => !_isTriggered && !_isCancelled; } From aaaee03f8b53f6e95f82534c751bf96ddaab407e Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Tue, 18 Nov 2025 19:30:38 +0100 Subject: [PATCH 2/5] root-zone real timers --- app/lib/task/clock_control.dart | 51 ++------------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/app/lib/task/clock_control.dart b/app/lib/task/clock_control.dart index 37449c785..6c95e4ad5 100644 --- a/app/lib/task/clock_control.dart +++ b/app/lib/task/clock_control.dart @@ -24,52 +24,7 @@ Future withClockControl( () async => await fn(clockCtrl), ), zoneSpecification: ZoneSpecification( - run: (self, parent, zone, R Function() fn) { - if (R is Future) { - return parent.run(zone, () => clock.instant(() async => fn())) as R; - } else { - return parent.run(zone, fn); - } - }, - runUnary: (self, parent, zone, R Function(T1) fn, T1 arg1) { - if (R is Future) { - return parent.runUnary( - zone, - (arg1) => clock.instant(() async => fn(arg1)) as R, - arg1, - ); - } else { - return parent.runUnary(zone, fn, arg1); - } - }, - runBinary: - ( - self, - parent, - zone, - R Function(T1, T2) fn, - T1 arg1, - T2 arg2, - ) { - if (R is Future) { - return parent.runBinary( - zone, - (arg1, arg2) => - clock.instant(() async => fn(arg1, arg2)) as R, - arg1, - arg2, - ); - } else { - return parent.runBinary(zone, fn, arg1, arg2); - } - }, createTimer: (self, parent, zone, duration, f) { - final current = StackTrace.current.toString(); - if (current.contains('ClockController._createTimer') || - current.contains('ClockController._triggerPendingTimers') || - current.contains('ClockController._cancelPendingTimer')) { - return parent.createTimer(zone, duration, f); - } final clockCtrl = zone[_clockCtrlKey]; if (clockCtrl is ClockController) { return clockCtrl._createTimer(duration, f); @@ -202,7 +157,7 @@ final class ClockController { // to create a new [_timerForFirstPendingTimer] timer. if (_pendingTimers.first == timer) { _timerForFirstPendingTimer?.cancel(); - _timerForFirstPendingTimer = timer._zone.createTimer( + _timerForFirstPendingTimer = Zone.root.createTimer( timer._duration, _triggerPendingTimers, ); @@ -243,7 +198,7 @@ final class ClockController { delay = Duration.zero; } - _timerForFirstPendingTimer = nextTimer._zone.createTimer( + _timerForFirstPendingTimer = Zone.root.createTimer( delay, _triggerPendingTimers, ); @@ -289,7 +244,7 @@ final class ClockController { delay = Duration.zero; } - _timerForFirstPendingTimer = nextTimer._zone.createTimer( + _timerForFirstPendingTimer = Zone.root.createTimer( delay, _triggerPendingTimers, ); From 946e09e4995a6fee3dce2d7e4befc9eaa7684f2d Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Wed, 19 Nov 2025 14:09:45 +0100 Subject: [PATCH 3/5] Implement periodic traveling timer. --- app/lib/task/clock_control.dart | 70 ++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/app/lib/task/clock_control.dart b/app/lib/task/clock_control.dart index 6c95e4ad5..3fd971787 100644 --- a/app/lib/task/clock_control.dart +++ b/app/lib/task/clock_control.dart @@ -27,11 +27,29 @@ Future withClockControl( createTimer: (self, parent, zone, duration, f) { final clockCtrl = zone[_clockCtrlKey]; if (clockCtrl is ClockController) { - return clockCtrl._createTimer(duration, f); + return clockCtrl._createTimer(duration, (_) => f()); } else { return parent.createTimer(zone, duration, f); } }, + createPeriodicTimer: (self, parent, zone, duration, f) { + final clockCtrl = zone[_clockCtrlKey]; + if (clockCtrl is ClockController) { + late _TravelingTimer timer; + timer = clockCtrl._createTimer(duration, (_) { + try { + f(timer); + } finally { + if (timer.isActive) { + timer._reschedule(); + } + } + }, periodic: true); + return timer; + } else { + return parent.createPeriodicTimer(zone, duration, f); + } + }, ), ), zoneValues: {_clockCtrlKey: clockCtrl}, @@ -54,7 +72,7 @@ extension FutureTimeout on Future { final clockCtrl = Zone.current[_clockCtrlKey]; if (clockCtrl is ClockController) { final c = Completer(); - final timer = clockCtrl._createTimer(timeLimit, () { + final timer = clockCtrl._createTimer(timeLimit, (_) { if (!c.isCompleted) { if (onTimeout != null) { c.complete(onTimeout()); @@ -97,7 +115,7 @@ extension ClockDelayed on Clock { final clockCtrl = Zone.current[_clockCtrlKey]; if (clockCtrl is ClockController) { final c = Completer(); - clockCtrl._createTimer(delay, c.complete); + clockCtrl._createTimer(delay, (_) => c.complete()); return c.future; } else { return Future.delayed(delay); @@ -142,15 +160,24 @@ final class ClockController { /// This value is `null` when [_pendingTimers] is empty. Timer? _timerForFirstPendingTimer; - _TravelingTimer _createTimer(Duration duration, void Function() fn) { + _TravelingTimer _createTimer( + Duration duration, + void Function(Timer) fn, { + bool periodic = false, + }) { final timer = _TravelingTimer( owner: this, createdInControlledTime: _controlledTime(), zone: Zone.current, duration: duration, trigger: fn, + periodic: periodic, ); + _appendTimer(timer); + return timer; + } + void _appendTimer(_TravelingTimer timer) { _pendingTimers.add(timer); // If the newly added [timer] is the first timer in the queue, then we have @@ -162,8 +189,6 @@ final class ClockController { _triggerPendingTimers, ); } - - return timer; } /// This will cancel [_timerForFirstPendingTimer] if active, and trigger all @@ -500,21 +525,24 @@ final class _TravelingTimer implements Timer { /// Duration for the timer to trigger. final Duration _duration; - final void Function() _triggerFn; + final void Function(Timer) _triggerFn; + final bool _periodic; /// Callback to be invoked when this [_TravelingTimer] is triggered. void _trigger(_TravelingTimer timer) { - if (isActive) { - _isTriggered = true; - _triggerFn(); + if (_isCancelled) { + return; } + if (!_periodic) { + _isCancelled = true; + } + _triggerFn(this); } /// [DateTime] when this [_TravelingTimer] is supposed to be triggered, /// measured in [ClockController._controlledTime]. DateTime get _elapsesAtInFakeTime => _createdInControlledTime.add(_duration); - bool _isTriggered = false; bool _isCancelled = false; _TravelingTimer({ @@ -522,12 +550,26 @@ final class _TravelingTimer implements Timer { required DateTime createdInControlledTime, required Zone zone, required Duration duration, - required void Function() trigger, + required void Function(Timer) trigger, + required bool periodic, }) : _owner = owner, _createdInControlledTime = createdInControlledTime, _zone = zone, _duration = duration, - _triggerFn = trigger; + _triggerFn = trigger, + _periodic = periodic; + + void _reschedule() { + final newInstance = _TravelingTimer( + owner: _owner, + createdInControlledTime: _owner._controlledTime(), + zone: _zone, + duration: _duration, + trigger: _triggerFn, + periodic: _periodic, + ); + _owner._appendTimer(newInstance); + } @override void cancel() { @@ -539,5 +581,5 @@ final class _TravelingTimer implements Timer { int get tick => throw UnimplementedError(); @override - bool get isActive => !_isTriggered && !_isCancelled; + bool get isActive => !_isCancelled; } From f4a7b58e95eea77d0dd555697ea2209d80115cec Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Mon, 24 Nov 2025 09:41:42 +0100 Subject: [PATCH 4/5] Timers in root zone. --- app/lib/task/clock_control.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/lib/task/clock_control.dart b/app/lib/task/clock_control.dart index 3fd971787..b88a119cf 100644 --- a/app/lib/task/clock_control.dart +++ b/app/lib/task/clock_control.dart @@ -495,7 +495,9 @@ final class ClockController { /// Wait for all scheduled microtasks to be done. Future _waitForMicroTasks() async { - await Future.microtask(() {}); + await Zone.root.run(() async { + await Future.delayed(Duration.zero); + }); while (_pendingInstants.isNotEmpty) { final f = Future.wait(_pendingInstants); @@ -506,7 +508,9 @@ final class ClockController { // ignore } - await Future.microtask(() {}); + await Zone.root.run(() async { + await Future.delayed(Duration.zero); + }); } } } From 6d66a68e22eac051e26777f56b4f9b0610b71d41 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 27 Nov 2025 08:58:43 +0100 Subject: [PATCH 5/5] Fix instant processing. --- app/lib/task/clock_control.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/lib/task/clock_control.dart b/app/lib/task/clock_control.dart index b88a119cf..c19a9c0b4 100644 --- a/app/lib/task/clock_control.dart +++ b/app/lib/task/clock_control.dart @@ -500,8 +500,7 @@ final class ClockController { }); while (_pendingInstants.isNotEmpty) { - final f = Future.wait(_pendingInstants); - _pendingInstants.clear(); + final f = _pendingInstants.removeLast(); try { await f; } catch (_) {