include/boost/corosio/native/detail/iocp/win_timers_nt.hpp

0.0% Lines (0/37) 0.0% Functions (0/5) 0.0% Branches (0/24)
include/boost/corosio/native/detail/iocp/win_timers_nt.hpp
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
3 // Copyright (c) 2026 Steve Gerbino
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP
12 #define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP
13
14 #include <boost/corosio/detail/platform.hpp>
15
16 #if BOOST_COROSIO_HAS_IOCP
17
18 #include <boost/corosio/detail/config.hpp>
19 #include <boost/corosio/native/detail/iocp/win_timers.hpp>
20 #include <boost/corosio/native/detail/iocp/win_completion_key.hpp>
21 #include <boost/corosio/native/detail/iocp/win_windows.hpp>
22
23 namespace boost::corosio::detail {
24
25 // NT API type definitions
26 using NTSTATUS = LONG;
27
28 using NtAssociateWaitCompletionPacketFn = NTSTATUS(NTAPI*)(
29 void* WaitCompletionPacketHandle,
30 void* IoCompletionHandle,
31 void* TargetObjectHandle,
32 void* KeyContext,
33 void* ApcContext,
34 NTSTATUS IoStatus,
35 ULONG_PTR IoStatusInformation,
36 BOOLEAN* AlreadySignaled);
37
38 using NtCancelWaitCompletionPacketFn = NTSTATUS(NTAPI*)(
39 void* WaitCompletionPacketHandle, BOOLEAN RemoveSignaledPacket);
40
41 class win_timers_nt final : public win_timers
42 {
43 void* iocp_;
44 void* waitable_timer_ = nullptr;
45 void* wait_packet_ = nullptr;
46 NtAssociateWaitCompletionPacketFn nt_associate_;
47 NtCancelWaitCompletionPacketFn nt_cancel_;
48
49 win_timers_nt(
50 void* iocp,
51 long* dispatch_required,
52 NtAssociateWaitCompletionPacketFn nt_assoc,
53 NtCancelWaitCompletionPacketFn nt_cancel);
54
55 public:
56 // Returns nullptr if NT APIs unavailable (pre-Windows 8)
57 static std::unique_ptr<win_timers_nt>
58 try_create(void* iocp, long* dispatch_required);
59
60 ~win_timers_nt();
61
62 win_timers_nt(win_timers_nt const&) = delete;
63 win_timers_nt& operator=(win_timers_nt const&) = delete;
64
65 void start() override;
66 void stop() override;
67 void update_timeout(time_point next_expiry) override;
68
69 private:
70 void associate_timer();
71 };
72
73 /*
74 NT Wait Completion Packet Timer Implementation
75 ==============================================
76
77 This uses undocumented NT APIs to integrate waitable timers directly with
78 IOCP, avoiding the need for a dedicated timer thread.
79
80 CRITICAL: THE ASSOCIATION IS ONE-SHOT
81 -------------------------------------
82
83 When NtAssociateWaitCompletionPacket associates a timer with IOCP, the
84 association is consumed when the timer fires. After the completion packet
85 is posted to IOCP, the wait packet is "spent" and must be re-associated
86 before it can fire again.
87
88 This means update_timeout() MUST be called after every timer wakeup to
89 re-associate the wait packet, even if the timer expiry hasn't changed.
90 The scheduler calls update_timeout() unconditionally in do_one() after
91 processing expired timers for this reason.
92
93 WHY THIS IMPLEMENTATION IS SLOW
94 --------------------------------
95
96 The re-association must happen on every scheduler iteration, even for
97 timer-free workloads. This causes ~60% CPU overhead in benchmarks because
98 SetWaitableTimer + NtAssociateWaitCompletionPacket are called repeatedly.
99
100 DO NOT OPTIMIZE BY SKIPPING RE-ASSOCIATION
101 ------------------------------------------
102
103 It may seem obvious to skip re-association when no timers exist or when the
104 expiry hasn't changed. However, skipping breaks the timer mechanism:
105
106 1. Timer fires -> posts key_wake_dispatch to IOCP
107 2. do_one() processes the completion, calls process_expired()
108 3. If update_timeout() is skipped, the wait packet is not re-associated
109 4. Future timers will never fire -> scheduler hangs
110
111 The correct optimization (if needed) would be at the waitable timer level
112 (caching due_time to avoid redundant SetWaitableTimer calls), but the
113 NtAssociateWaitCompletionPacket call cannot be skipped after any wakeup.
114 */
115
116 inline constexpr NTSTATUS STATUS_SUCCESS = 0;
117
118 using NtCreateWaitCompletionPacketFn = NTSTATUS(NTAPI*)(
119 void** WaitCompletionPacketHandle,
120 ULONG DesiredAccess,
121 void* ObjectAttributes);
122
123 inline win_timers_nt::win_timers_nt(
124 void* iocp,
125 long* dispatch_required,
126 NtAssociateWaitCompletionPacketFn nt_assoc,
127 NtCancelWaitCompletionPacketFn nt_cancel)
128 : win_timers(dispatch_required)
129 , iocp_(iocp)
130 , nt_associate_(nt_assoc)
131 , nt_cancel_(nt_cancel)
132 {
133 waitable_timer_ = ::CreateWaitableTimerW(nullptr, FALSE, nullptr);
134 }
135
136 inline std::unique_ptr<win_timers_nt>
137 win_timers_nt::try_create(void* iocp, long* dispatch_required)
138 {
139 HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll");
140 if (!ntdll)
141 return nullptr;
142
143 auto nt_create = reinterpret_cast<NtCreateWaitCompletionPacketFn>(
144 ::GetProcAddress(ntdll, "NtCreateWaitCompletionPacket"));
145 auto nt_assoc = reinterpret_cast<NtAssociateWaitCompletionPacketFn>(
146 ::GetProcAddress(ntdll, "NtAssociateWaitCompletionPacket"));
147 auto nt_cancel = reinterpret_cast<NtCancelWaitCompletionPacketFn>(
148 ::GetProcAddress(ntdll, "NtCancelWaitCompletionPacket"));
149
150 if (!nt_create || !nt_assoc || !nt_cancel)
151 return nullptr;
152
153 auto p = std::unique_ptr<win_timers_nt>(
154 new win_timers_nt(iocp, dispatch_required, nt_assoc, nt_cancel));
155
156 if (!p->waitable_timer_)
157 return nullptr;
158
159 // Create the wait completion packet
160 NTSTATUS status = nt_create(&p->wait_packet_, MAXIMUM_ALLOWED, nullptr);
161 if (status != STATUS_SUCCESS || !p->wait_packet_)
162 return nullptr;
163
164 return p;
165 }
166
167 inline win_timers_nt::~win_timers_nt()
168 {
169 if (wait_packet_)
170 ::CloseHandle(wait_packet_);
171 if (waitable_timer_)
172 ::CloseHandle(waitable_timer_);
173 }
174
175 inline void
176 win_timers_nt::start()
177 {
178 associate_timer();
179 }
180
181 inline void
182 win_timers_nt::stop()
183 {
184 nt_cancel_(wait_packet_, TRUE);
185 }
186
187 inline void
188 win_timers_nt::update_timeout(time_point next_expiry)
189 {
190 BOOST_COROSIO_ASSERT(waitable_timer_);
191
192 // Cancel pending association
193 nt_cancel_(wait_packet_, FALSE);
194
195 auto now = std::chrono::steady_clock::now();
196 LARGE_INTEGER due_time;
197
198 if (next_expiry <= now)
199 {
200 // Already expired - fire immediately
201 due_time.QuadPart = 0;
202 }
203 else if (next_expiry == (time_point::max)())
204 {
205 // No timers - set far future
206 due_time.QuadPart = -LONGLONG(49) * 24 * 60 * 60 * 10000000LL;
207 }
208 else
209 {
210 // Convert duration to 100ns units (negative = relative)
211 auto duration = next_expiry - now;
212 auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(duration)
213 .count();
214 due_time.QuadPart = -(ns / 100);
215 if (due_time.QuadPart == 0)
216 due_time.QuadPart = -1;
217 }
218
219 ::SetWaitableTimer(waitable_timer_, &due_time, 0, nullptr, nullptr, FALSE);
220 associate_timer();
221 }
222
223 inline void
224 win_timers_nt::associate_timer()
225 {
226 // Set dispatch flag before associating
227 ::InterlockedExchange(dispatch_required_, 1);
228
229 BOOLEAN already_signaled = FALSE;
230 NTSTATUS status = nt_associate_(
231 wait_packet_, iocp_, waitable_timer_,
232 reinterpret_cast<void*>(key_wake_dispatch), nullptr, STATUS_SUCCESS, 0,
233 &already_signaled);
234
235 if (status == STATUS_SUCCESS && already_signaled)
236 {
237 ::PostQueuedCompletionStatus(
238 static_cast<HANDLE>(iocp_), 0, key_wake_dispatch, nullptr);
239 }
240 }
241
242 } // namespace boost::corosio::detail
243
244 #endif // BOOST_COROSIO_HAS_IOCP
245
246 #endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP
247