Skip to content

Commit 77a581a

Browse files
authored
[win32] Runloop should use high resolution timer and avoid deadlock (flutter#176023)
Replaces `SetTimer`/`WM_TIMER` with a threadpool timer, which doesn't suffer from 16ms granularity and changes the waking up mechanism to give the run loop chance to process input messages before processing flutter tasks. - Fixes flutter#173843 - Fixes flutter#175135 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 7519cfd commit 77a581a

3 files changed

Lines changed: 134 additions & 13 deletions

File tree

engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,76 @@ TEST_F(FlutterWindowsEngineTest, RunHeadless) {
6464
ASSERT_EQ(engine->view(123), nullptr);
6565
}
6666

67+
TEST_F(FlutterWindowsEngineTest, TaskRunnerDelayedTask) {
68+
bool finished = false;
69+
auto runner = std::make_unique<TaskRunner>(
70+
[] {
71+
return static_cast<uint64_t>(
72+
fml::TimePoint::Now().ToEpochDelta().ToNanoseconds());
73+
},
74+
[&](const FlutterTask*) { finished = true; });
75+
runner->PostFlutterTask(
76+
FlutterTask{},
77+
static_cast<uint64_t>((fml::TimePoint::Now().ToEpochDelta() +
78+
fml::TimeDelta::FromMilliseconds(50))
79+
.ToNanoseconds()));
80+
auto start = fml::TimePoint::Now();
81+
while (!finished) {
82+
PumpMessage();
83+
}
84+
auto duration = fml::TimePoint::Now() - start;
85+
EXPECT_GE(duration, fml::TimeDelta::FromMilliseconds(50));
86+
}
87+
88+
// https://github.com/flutter/flutter/issues/173843)
89+
TEST_F(FlutterWindowsEngineTest, TaskRunnerDoesNotDeadlock) {
90+
auto runner = std::make_unique<TaskRunner>(
91+
[] {
92+
return static_cast<uint64_t>(
93+
fml::TimePoint::Now().ToEpochDelta().ToNanoseconds());
94+
},
95+
[&](const FlutterTask*) {});
96+
97+
struct RunnerHolder {
98+
void PostTaskLoop() {
99+
runner->PostTask([this] { PostTaskLoop(); });
100+
}
101+
std::unique_ptr<TaskRunner> runner;
102+
};
103+
104+
RunnerHolder container{.runner = std::move(runner)};
105+
// Spam flutter tasks.
106+
container.PostTaskLoop();
107+
108+
const LPCWSTR class_name = L"FlutterTestWindowClass";
109+
WNDCLASS wc = {0};
110+
wc.lpfnWndProc = DefWindowProc;
111+
wc.lpszClassName = class_name;
112+
RegisterClass(&wc);
113+
114+
HWND window;
115+
container.runner->PostTask([&] {
116+
window = CreateWindowEx(0, class_name, L"Empty Window", WS_OVERLAPPEDWINDOW,
117+
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr,
118+
nullptr, nullptr, nullptr);
119+
ShowWindow(window, SW_SHOW);
120+
});
121+
122+
while (true) {
123+
::MSG msg;
124+
if (::GetMessage(&msg, nullptr, 0, 0)) {
125+
if (msg.message == WM_PAINT) {
126+
break;
127+
}
128+
::TranslateMessage(&msg);
129+
::DispatchMessage(&msg);
130+
}
131+
}
132+
133+
DestroyWindow(window);
134+
UnregisterClassW(class_name, nullptr);
135+
}
136+
67137
TEST_F(FlutterWindowsEngineTest, RunDoesExpectedInitialization) {
68138
FlutterWindowsEngineBuilder builder{GetContext()};
69139
builder.AddDartEntrypointArgument("arg1");

engine/src/flutter/shell/platform/windows/task_runner_window.cc

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
#include "flutter/shell/platform/windows/task_runner_window.h"
66

7+
#include <timeapi.h>
78
#include <algorithm>
9+
#include <chrono>
810

911
#include "flutter/fml/logging.h"
1012

1113
namespace flutter {
1214

13-
static const uintptr_t kTimerId = 0;
14-
1515
// Timer used for PollOnce timeout.
1616
static const uintptr_t kPollTimeoutTimerId = 1;
1717

@@ -21,6 +21,13 @@ TaskRunnerWindow::TaskRunnerWindow() {
2121
CreateWindowEx(0, window_class.lpszClassName, L"", 0, 0, 0, 0, 0,
2222
HWND_MESSAGE, nullptr, window_class.hInstance, nullptr);
2323

24+
timer_ = CreateThreadpoolTimer(TimerProc, this, nullptr);
25+
if (!timer_) {
26+
FML_LOG(ERROR) << "Failed to create threadpool timer, error: "
27+
<< GetLastError();
28+
FML_CHECK(timer_);
29+
}
30+
2431
if (window_handle_) {
2532
SetWindowLongPtr(window_handle_, GWLP_USERDATA,
2633
reinterpret_cast<LONG_PTR>(this));
@@ -35,14 +42,40 @@ TaskRunnerWindow::TaskRunnerWindow() {
3542
OutputDebugString(message);
3643
LocalFree(message);
3744
}
45+
46+
thread_id_ = GetCurrentThreadId();
47+
48+
// Increase timer precision for this process (the call only affects
49+
// current process since Windows 10, version 2004).
50+
timeBeginPeriod(1);
3851
}
3952

4053
TaskRunnerWindow::~TaskRunnerWindow() {
54+
SetThreadpoolTimer(timer_, nullptr, 0, 0);
55+
// Ensures that no callbacks will run after CloseThreadpoolTimer.
56+
// https://learn.microsoft.com/en-us/windows/win32/api/threadpoolapiset/nf-threadpoolapiset-closethreadpooltimer#remarks
57+
WaitForThreadpoolTimerCallbacks(timer_, TRUE);
58+
CloseThreadpoolTimer(timer_);
59+
4160
if (window_handle_) {
4261
DestroyWindow(window_handle_);
4362
window_handle_ = nullptr;
4463
}
4564
UnregisterClass(window_class_name_.c_str(), nullptr);
65+
66+
timeEndPeriod(1);
67+
}
68+
69+
void TaskRunnerWindow::OnTimer() {
70+
if (!PostMessage(window_handle_, WM_NULL, 0, 0)) {
71+
FML_LOG(ERROR) << "Failed to post message to main thread.";
72+
}
73+
}
74+
75+
void TaskRunnerWindow::TimerProc(PTP_CALLBACK_INSTANCE instance,
76+
PVOID context,
77+
PTP_TIMER timer) {
78+
reinterpret_cast<TaskRunnerWindow*>(context)->OnTimer();
4679
}
4780

4881
std::shared_ptr<TaskRunnerWindow> TaskRunnerWindow::GetSharedInstance() {
@@ -57,6 +90,18 @@ std::shared_ptr<TaskRunnerWindow> TaskRunnerWindow::GetSharedInstance() {
5790
}
5891

5992
void TaskRunnerWindow::WakeUp() {
93+
// When waking up from main thread while there are messages in the message
94+
// queue use timer to post the WM_NULL message from background thread. This
95+
// gives message loop chance to process input events before WM_NULL is
96+
// processed - which is necessary because messages scheduled through
97+
// PostMessage take precedence over input event messages. Otherwise await
98+
// Future.delayed(Duration.zero) deadlocks the main thread. (See
99+
// https://github.com/flutter/flutter/issues/173843)
100+
if (thread_id_ == GetCurrentThreadId() && GetQueueStatus(QS_ALLEVENTS) != 0) {
101+
SetTimer(std::chrono::nanoseconds::zero());
102+
return;
103+
}
104+
60105
if (!PostMessage(window_handle_, WM_NULL, 0, 0)) {
61106
FML_LOG(ERROR) << "Failed to post message to main thread.";
62107
}
@@ -99,10 +144,16 @@ void TaskRunnerWindow::ProcessTasks() {
99144

100145
void TaskRunnerWindow::SetTimer(std::chrono::nanoseconds when) {
101146
if (when == std::chrono::nanoseconds::max()) {
102-
KillTimer(window_handle_, kTimerId);
147+
SetThreadpoolTimer(timer_, nullptr, 0, 0);
103148
} else {
104-
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(when);
105-
::SetTimer(window_handle_, kTimerId, millis.count() + 1, nullptr);
149+
auto microseconds =
150+
std::chrono::duration_cast<std::chrono::microseconds>(when).count();
151+
ULARGE_INTEGER ticks;
152+
ticks.QuadPart = -static_cast<LONGLONG>(microseconds * 10);
153+
FILETIME ft;
154+
ft.dwLowDateTime = ticks.LowPart;
155+
ft.dwHighDateTime = ticks.HighPart;
156+
SetThreadpoolTimer(timer_, &ft, 0, 0);
106157
}
107158
}
108159

@@ -129,14 +180,6 @@ TaskRunnerWindow::HandleMessage(UINT const message,
129180
WPARAM const wparam,
130181
LPARAM const lparam) noexcept {
131182
switch (message) {
132-
case WM_TIMER:
133-
if (wparam == kPollTimeoutTimerId) {
134-
// Ignore PollOnce timeout timer.
135-
return 0;
136-
}
137-
FML_DCHECK(wparam == kTimerId);
138-
ProcessTasks();
139-
return 0;
140183
case WM_NULL:
141184
ProcessTasks();
142185
return 0;

engine/src/flutter/shell/platform/windows/task_runner_window.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,17 @@ class TaskRunnerWindow {
5959
WPARAM const wparam,
6060
LPARAM const lparam) noexcept;
6161

62+
void OnTimer();
63+
64+
static void TimerProc(PTP_CALLBACK_INSTANCE Instance,
65+
PVOID Context,
66+
PTP_TIMER Timer);
67+
6268
HWND window_handle_;
6369
std::wstring window_class_name_;
6470
std::vector<Delegate*> delegates_;
71+
PTP_TIMER timer_ = nullptr;
72+
DWORD thread_id_ = 0;
6573

6674
FML_DISALLOW_COPY_AND_ASSIGN(TaskRunnerWindow);
6775
};

0 commit comments

Comments
 (0)