-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Possible wrong timer behavoir in run_wasm.js #59666
Comments
Summary: The |
Thanks for reporting. I don't understand how these callbacks can execute out of order given that earlier callbacks schedule the later ones: void scheduleFrame() {
_frameTimer ??= Timer(
const Duration(milliseconds: 16),
() {
if (_continue) {
_frameTimer = null;
}
final int microseconds = _frameTime.inMicroseconds;
_frameTime += const Duration(milliseconds: 16);
Timer.run(() {
final Stopwatch beginSw = Stopwatch()..start();
_beginFrame(microseconds);
beginSw.stop();
Timer.run(() {
final Stopwatch drawSw = Stopwatch()..start();
_drawFrame();
drawSw.stop();
_continue = frameCallback(
_frameCount,
beginSw.elapsedMicroseconds / 1000,
drawSw.elapsedMicroseconds / 1000);
_frameCount += 1;
});
});
},
);
} Here Since there will be one callback scheduled by |
I observe the failure in the benchmark only when I run it under valgrind. It is reproducible with debug builds of the JS engine.
perhaps _beginFrame,_beginFrame,_drawFrame,_drawFrame The Sorry, I wish I could be more helpful, I'm mostly working with wasm and js artifacts. |
Thanks, @yurydelendik ! I can reproduce this quite easily by adding a busy loop after the build frame. The root cause is not a bug in the timer implementation. The timer implementation is just running already expired timers (i.e. timers with expiration time before the current time) before new callbacks registered with The reason this happens is in the flute code. When it schedules a frame callback it will run the tl;dr The next frame is scheduled in the The reason flute is written in that way is that between So I think we can fix this by scheduling the diff --git a/engine/lib/src/platform_dispatcher.dart b/engine/lib/src/platform_dispatcher.dart
index c8cc92c..64af62a 100644
--- a/engine/lib/src/platform_dispatcher.dart
+++ b/engine/lib/src/platform_dispatcher.dart
@@ -574,9 +574,7 @@ class PlatformDispatcher {
final int microseconds = _frameTime.inMicroseconds;
_frameTime += const Duration(milliseconds: 16);
Timer.run(() {
- final Stopwatch beginSw = Stopwatch()..start();
- _beginFrame(microseconds);
- beginSw.stop();
+ late final Stopwatch beginSw;
Timer.run(() {
final Stopwatch drawSw = Stopwatch()..start();
_drawFrame();
@@ -587,6 +585,9 @@ class PlatformDispatcher {
drawSw.elapsedMicroseconds / 1000);
_frameCount += 1;
});
+ beginSw = Stopwatch()..start();
+ _beginFrame(microseconds);
+ beginSw.stop();
});
},
); |
For reference, here's the real C++ implementation tonic::DartInvoke(begin_frame_.Get(), ...);
UIDartState::Current()->FlushMicrotasksNow();
tonic::DartInvokeVoid(draw_frame_.Get()); which doesn't allow any timer events to come in-between |
That heap looks fine. It's a heap priority queue, so it implicitly represents subtrees through the indices. The child nodes of the node at position i are at positions i×2+1 and i×2+2. Here it makes The way the timer queue implementation works is that it has a "due timers" list that it takes events from, and a heap priority queue of later timers, each a list containing the time they are due and a number of timers due at that time. When the due timers list becomes empty, and a the next timer time in the heap is due, it's popped out of the heap, which is then normalized to put the next smallest time at the root. This means that all timers in the due-timer list are scheduled for before the current time. No new timer can be scheduled before them, because no new timer can be scheduled before the current time, it's no earlier than zero ms after the current So when a new timer is added, it's added to the delayed timers priority heap, which puts it into the position it needs to be relative to all other existing scheduled timers. If any of those scheduled timers are also already due (but originally scheduled later than the ones in the due-timer list), the new event won't become the root of the priority heap. The text Without the The fix of if (ms == 0){
if (!isNextTimerDue()) {
zeroTimerQueue.push(f);
} else {
timerHeap[0].push(f); // keeps Timer.run together
}
} else { is wrong too. It puts the zero-timer with the first due timers in the heap, but if there is more than one due timer-time in the heap, that will run the new timer before the second group of already due timers, which is incorrect. If your timer events come from a 16ms interval timer, which do some work and schedule a zero-timer event to do more work and schedule a second zero-timer event, then it's possible that the last zero-timer event has been delayed so much that it's scheduled after the next 16 ms interval timer. |
A `_beginFrame()` seems to schedule the next frame interanally. If the `_beginFrame()` takes a long time, it will finish after the expiration date of the next frame timer. That in return makes a `Timer.run(() => _drawFrame())` be scheduled *after* the next frame callback is already run. Though we want to ensure the `_beginFrame()` and `_drawFrame()` are together before starting to work on the next frame. We only want microtasks to be drained in-between those two. (This is how C++ engine works) We can get similar semantics by scheduling the `_drawFrame()` timer even before running the `_beginFrame()` code. Issue dart-lang#23 Issue dart-lang/sdk#59666
(As a solution, you may want to not use an interval timer, and to schedule the next event yourself, when the former event is done running. That way you know that there won't be an existing timer with a lower due time already in the timer queue. You'll have to decide when to run the next event then, if the current event has taken more than 16 ms to run.) |
Have sent out dart-lang/flute#24 which I think is sufficient to fix this issue. |
…e `_beginFrame()` takes a long time, it will finish after the expiration date of the next frame timer. That in return makes a `Timer.run(() => _drawFrame())` be scheduled *after* the next frame callback is already run. Though we want to ensure the `_beginFrame()` and `_drawFrame()` are together before starting to work on the next frame. We only want microtasks to be drained in-between those two. (This is how C++ engine works) We can get similar semantics by scheduling the `_drawFrame()` timer even before running the `_beginFrame()` code. Issue #23 Issue dart-lang/sdk#59666 GitOrigin-RevId: 71e3edb Change-Id: Ib2eb54d159d59a4e8e78a71682628160b66ff897 Reviewed-on: https://dart-review.googlesource.com/c/flute/+/399380 Reviewed-by: Lasse Nielsen <[email protected]> Reviewed-by: Alexander Thomas <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
The flute benchmark was fixed some time ago. We've rolled that fix into our CI & benchmarking infrastructure some time ago. Today I've also rolled it into https://github.com/mkustermann/wasm_gc_benchmarks, so this should close this bug. |
TL;DR I suspect a defect in the implementation of the addTimer code:
sdk/pkg/dart2wasm/bin/run_wasm.js
Line 145 in f53c758
It is connected with
isNextTimerDue
logic: whenms == 0
andisNextTimerDue() == true
, the Task.run may create two separate entries in the timerHeap (in comparison withisNextTimerDue() == false
, they will be just added tozeroTimerQueue
) when engine runs slow.The flute engine produces timers with the following cadence: 16 0 0 16 0 0 .. in the scheduleFrame. The defect above causing dart benchmark to fail "Null check operator used on a null value" because the draw frame can be executed out of sequence with corresponding begin frame.
I'm temporary patching it with:
The text was updated successfully, but these errors were encountered: