Repository navigation
Description
Failure log
[111/408] ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback (2209 ms)
[INFO:flutter/testing/test_timeout_listener.cc(75)] Test timeout of 300 seconds per test case will be enforced.
[0;33mNote: Google Test filter = ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback
[m [0;32m[==========] [mRunning 1 test from 1 test suite.
[0;32m[----------] [mGlobal test environment set-up.
[0;32m[----------] [m1 test from ShellTest
[0;32m[ RUN ] [mShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback
../../../flutter/shell/common/shell_unittests.cc:2322: Failure
Value of: is_on_begin_frame_called
Actual: false
Expected: true
../../../flutter/shell/common/shell_unittests.cc:2299: Failure
Value of: is_secondary_callback_called
Actual: true
Expected: false
[0;31m[ FAILED ] [mShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback (971 ms)
[0;32m[----------] [m1 test from ShellTest (971 ms total)
Gemini's analysis below so take this with a grain of salt.
Description
ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback in engine/src/flutter/shell/common/shell_unittests.cc:2282-2334 is flaky under heavy load.
When running the test in isolation (--gtest_filter=ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback), it passes consistently in ~37 ms on an M3 Mac Studio Ultra. However, under high CPU load or when running the full shell_unittests suite, it fails intermittently with the following assertion errors:
../../../flutter/shell/common/shell_unittests.cc:2322: Failure
Value of: is_on_begin_frame_called
Actual: false
Expected: true
../../../flutter/shell/common/shell_unittests.cc:2299: Failure
Value of: is_secondary_callback_called
Actual: true
Expected: false
[ FAILED ] ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback (971 ms)
Root Causes
The test failures show that the secondary vsync callback executed before Dart's onBeginFrame callback (NativeOnBeginFrame) was ever invoked.
There are two race conditions that cause this inversion under load:
1. Root Cause 1: PipelineFull in Animator::BeginFrame skips onBeginFrame while secondary callbacks still execute
When VsyncWaiter::FireCallback executes on a vsync interval, it sequentially posts two tasks to the UITaskRunner in FIFO order (vsync_waiter.cc:130-151):
P_task: The primary vsync callback, which invokesAnimator::BeginFrame(...).S_task: The secondary vsync callback(s).
When P_task runs on the UI thread, it executes Animator::BeginFrame (animator.cc:101-108):
if (!producer_continuation_) {
producer_continuation_ = layer_tree_pipeline_->Produce();
if (!producer_continuation_) {
TRACE_EVENT0("flutter", "PipelineFull");
RequestFrame();
return;
}
}- Under heavy test suite contention, the rasterizer thread can lag behind in consuming items from the 2-deep
layer_tree_pipeline_. - If all pipeline slots are occupied,
layer_tree_pipeline_->Produce()returnsnullptr(!producer_continuation_). - When
!producer_continuation_occurs,Animator::BeginFramelogsPipelineFulland returns immediately at line 107 without callingdelegate_.OnAnimatorBeginFrame(...). - Consequently, Dart's
onBeginFrame(NativeOnBeginFrame) is skipped for that vsync while the frame request is retried on the next tick. - However,
S_task(the secondary vsync callback) was already posted to theUITaskRunner. It executes immediately afterP_taskreturns, encounteringis_on_begin_frame_called == falseand failing the test at line 2322. - When the pipeline clears on a subsequent vsync,
NativeOnBeginFramefinally executes and fails at line 2299 becauseis_secondary_callback_calledwas already set totrue.
2. Root Cause 2: Asymmetric Registration Between ScheduleSecondaryVsyncCallback and Animator::RequestFrame
The test schedules two callbacks sequentially on the UITaskRunner:
shell->GetEngine()->ScheduleSecondaryVsyncCallback(0, [&]() { ... });
shell->GetEngine()->ScheduleFrame();ScheduleSecondaryVsyncCallbackis synchronous: It immediately callsVsyncWaiter::ScheduleSecondaryCallback(vsync_waiter.cc:53-85), which invokesAwaitVSyncForSecondaryCallback()->VsyncWaiterFallback::AwaitVSync(). This calculatesframe_start_timeviaSnapToNextTick(vsync_waiter_fallback.cc:16-24) and schedules a timer task (PostTaskForTime([FireCallback], frame_start_time)) on theUITaskRunner.ScheduleFrameis asynchronous:Engine::ScheduleFrame()callsAnimator::RequestFrame(true). Instead of callingwaiter_->AsyncWaitForVsync(...)synchronously,Animator::RequestFrame()posts an asynchronous task (PostTask([self->AwaitVSync()])) to theUITaskRunner(animator.cc:263-269). Only when that task runs does it callVsyncWaiter::AsyncWaitForVsyncto setcallback_.- Under thread starvation or when
frame_start_timesnaps toNow(), the timer task forVsyncWaiter::FireCallbackcan be dequeued beforeAnimator::AwaitVSync()has executed. - When
FireCallbackruns whilecallback_is stillnullptr, it dispatches only the secondary callback on the first vsync, causing it to execute ahead of any primary frame callback.
Why This Happens Under Test Suite Contention
In isolation, the UI, Raster, and Platform threads have zero contention. T_await_vsync registers callback_ well before the 16.66 ms vsync timer fires, and the rasterizer pipeline is empty.
During full test suite execution, heavy CPU contention and multi-thread load cause:
- Rasterizer pipeline backpressure (
PipelineFull), causingAnimator::BeginFrameto abort early and retry on the next vsync. - Message loop scheduling latency on
UITaskRunner, exposing the registration race betweenScheduleSecondaryVsyncCallbackandAnimator::RequestFrame.
Activity
cbracken commentedon Aug 3, 2026
/cc @gaaclarke
gaaclarke commentedon Aug 3, 2026
hey @cbracken, can you just reproduce this by using gtest_repeat? Or does it require something more to reproduce?
jason-simmons commentedon Aug 3, 2026
I can reproduce this on Linux by running the test repeatedly with the
gtest-parallelscript:The test occasionally fails with the "Root Cause 2" scenario. If the vsync happens between the test's call to
ScheduleSecondaryVsyncCallbackand its call toScheduleFrame, then theScheduleSecondaryVsyncCallbackcallback will execute before the frame callback.The test originated in flutter-team-archive/engine@ebcd86f681 and is apparently trying to verify that secondary callbacks run after the frame callback if both are scheduled for the same vsync tick.
The engine APIs do not offer a way to simulate that in a fully deterministic way. But the test can minimize the risk of this flake by executing the
ScheduleSecondaryVsyncCallbackandScheduleFramewithin anAsyncWaitForVsynccallback.AsyncWaitForVsyncwill run the callback shortly after a vsync, thus leaving plenty of time to make both API calls before the next vsync occurs.While trying to fix this I found another issue in
VsyncWaiter.VsyncWaiter::FireCallbackis doing acallback = std::move(callback_), but that is not guaranteed to resetcallback_to an empty state.This is not noticed in the engine because the
Animatoralways passes the same callback function toVsyncWaiter::AsyncWaitForVsync. But a test that callsAsyncWaitForVsyncwith multiple callback functions will get incorrect behavior if the previous callback is never cleared.Fix a flake in ShellTest.SecondaryVsyncCallbackShouldBeCalledAfterVsy…
github-actions commentedon Aug 19, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.