Skip to content

Commit b85e4bc

Browse files
authored
[k2] implement async stack (#1320)
1 parent 4540919 commit b85e4bc

19 files changed

Lines changed: 395 additions & 28 deletions
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2025 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include <coroutine>
8+
#include <utility>
9+
10+
/**
11+
* This header defines the data structures used to represent a coroutine asynchronous stack.
12+
*
13+
* Overview:
14+
* The asynchronous stack is used to manage the execution state of coroutines, allowing for
15+
* efficient context switching and stack management.
16+
*
17+
* Diagram: Normal and Asynchronous Stacks
18+
*
19+
* Base Pointer (%rbp) async_stack_root
20+
* | |
21+
* V V
22+
* stack_frame async_stack_frame
23+
* | |
24+
* V V
25+
* stack_frame async_stack_frame
26+
* ... ...
27+
* | |
28+
* V V
29+
* stack_frame async_stack_frame
30+
* | |
31+
* V V
32+
*
33+
* In the diagram above, the left side represents a typical call stack with stack frames linked by
34+
* the base pointer (%rbp). The right side illustrates an asynchronous stack where `async_stack_frame`
35+
* structures are linked by `async_stack_root`.
36+
*
37+
* Diagram: Backtrace Mechanism
38+
*
39+
* Base Pointer (%rbp)
40+
* |
41+
* V
42+
* stack_frame
43+
* |
44+
* V
45+
* stack_frame (stop_sync_frame) <- async_stack_root
46+
* |
47+
* V
48+
* async_stack_frame (top_async_frame)
49+
* |
50+
* V
51+
* async_stack_frame
52+
* ...
53+
* |
54+
* V
55+
* async_stack_frame
56+
*
57+
* The backtrace mechanism involves traversing the stack frames to capture the call stack.
58+
* The `stop_sync_frame` serves as a marker where the transition to the asynchronous stack occurs,
59+
* allowing the backtrace to continue through the `async_stack_frame` structures.
60+
*/
61+
62+
#define STACK_RETURN_ADDRESS __builtin_return_address(0)
63+
64+
#define STACK_FRAME_ADDRESS __builtin_frame_address(0)
65+
66+
namespace kphp::coro {
67+
68+
struct stack_frame {
69+
stack_frame* caller_stack_frame{};
70+
void* return_address{};
71+
};
72+
73+
struct async_stack_root;
74+
75+
struct async_stack_frame {
76+
async_stack_frame* caller_async_stack_frame{};
77+
async_stack_root* async_stack_root{};
78+
void* return_address{};
79+
};
80+
81+
struct async_stack_root {
82+
async_stack_frame* top_async_stack_frame{};
83+
stack_frame* stop_sync_stack_frame{};
84+
};
85+
86+
/**
87+
* The `resume` function is responsible for storing the current synchronous stack frame
88+
* in async_stack_root::stop_sync_frame before resuming the coroutine. This allows
89+
* capturing one of the stack frames in the synchronous stack trace.
90+
*/
91+
inline void resume(std::coroutine_handle<> handle, async_stack_root& stack_root) noexcept {
92+
auto* previous_stack_frame{std::exchange(stack_root.stop_sync_stack_frame, reinterpret_cast<stack_frame*>(STACK_FRAME_ADDRESS))};
93+
handle.resume();
94+
stack_root.stop_sync_stack_frame = previous_stack_frame;
95+
}
96+
97+
/**
98+
* The async_stack_element class facilitates working with asynchronous traces in templated code.
99+
* This allows for uniform handling of any coroutines in places where async frames are pushed or popped.
100+
*/
101+
struct async_stack_element {
102+
async_stack_frame& get_async_stack_frame() noexcept {
103+
return async_stack_frame_;
104+
}
105+
106+
private:
107+
async_stack_frame async_stack_frame_;
108+
};
109+
110+
} // namespace kphp::coro

runtime-light/coroutine/awaitable.h

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
#include <concepts>
99
#include <coroutine>
1010
#include <cstdint>
11+
#include <memory>
1112
#include <optional>
1213
#include <tuple>
1314
#include <type_traits>
1415
#include <utility>
1516

17+
#include "runtime-light/coroutine/async-stack.h"
18+
#include "runtime-light/coroutine/coroutine-state.h"
1619
#include "runtime-light/coroutine/shared-task.h"
1720
#include "runtime-light/coroutine/task.h"
1821
#include "runtime-light/k2-platform/k2-api.h"
@@ -55,9 +58,24 @@ class fork_id_watcher_t {
5558
}
5659
};
5760

61+
class async_stack_watcher_t {
62+
kphp::coro::async_stack_root* const async_stack_root;
63+
kphp::coro::async_stack_frame* const suspended_async_stack_frame;
64+
65+
protected:
66+
void await_resume() const noexcept {
67+
async_stack_root->top_async_stack_frame = suspended_async_stack_frame;
68+
}
69+
70+
public:
71+
async_stack_watcher_t() noexcept
72+
: async_stack_root(std::addressof(CoroutineInstanceState::get().coroutine_stack_root)),
73+
suspended_async_stack_frame(async_stack_root->top_async_stack_frame) {}
74+
};
75+
5876
} // namespace awaitable_impl_
5977

60-
class wait_for_update_t : public awaitable_impl_::fork_id_watcher_t {
78+
class wait_for_update_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
6179
uint64_t stream_d;
6280
SuspendToken suspend_token;
6381
awaitable_impl_::state state{awaitable_impl_::state::init};
@@ -96,6 +114,7 @@ class wait_for_update_t : public awaitable_impl_::fork_id_watcher_t {
96114

97115
constexpr void await_resume() noexcept {
98116
state = awaitable_impl_::state::end;
117+
async_stack_watcher_t::await_resume();
99118
fork_id_watcher_t::await_resume();
100119
}
101120

@@ -111,7 +130,7 @@ class wait_for_update_t : public awaitable_impl_::fork_id_watcher_t {
111130

112131
// ================================================================================================
113132

114-
class wait_for_incoming_stream_t : awaitable_impl_::fork_id_watcher_t {
133+
class wait_for_incoming_stream_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
115134
SuspendToken suspend_token{std::noop_coroutine(), WaitEvent::IncomingStream{}};
116135
awaitable_impl_::state state{awaitable_impl_::state::init};
117136

@@ -146,6 +165,7 @@ class wait_for_incoming_stream_t : awaitable_impl_::fork_id_watcher_t {
146165

147166
uint64_t await_resume() noexcept {
148167
state = awaitable_impl_::state::end;
168+
async_stack_watcher_t::await_resume();
149169
fork_id_watcher_t::await_resume();
150170
const auto incoming_stream_d{InstanceState::get().take_incoming_stream()};
151171
kphp::log::assertion(incoming_stream_d != k2::INVALID_PLATFORM_DESCRIPTOR);
@@ -164,7 +184,7 @@ class wait_for_incoming_stream_t : awaitable_impl_::fork_id_watcher_t {
164184

165185
// ================================================================================================
166186

167-
class wait_for_reschedule_t : awaitable_impl_::fork_id_watcher_t {
187+
class wait_for_reschedule_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
168188
SuspendToken suspend_token{std::noop_coroutine(), WaitEvent::Rechedule{}};
169189
awaitable_impl_::state state{awaitable_impl_::state::init};
170190

@@ -198,6 +218,7 @@ class wait_for_reschedule_t : awaitable_impl_::fork_id_watcher_t {
198218

199219
constexpr void await_resume() noexcept {
200220
state = awaitable_impl_::state::end;
221+
async_stack_watcher_t::await_resume();
201222
fork_id_watcher_t::await_resume();
202223
}
203224

@@ -213,7 +234,7 @@ class wait_for_reschedule_t : awaitable_impl_::fork_id_watcher_t {
213234

214235
// ================================================================================================
215236

216-
class wait_for_timer_t : awaitable_impl_::fork_id_watcher_t {
237+
class wait_for_timer_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
217238
std::chrono::nanoseconds duration;
218239
uint64_t timer_d{k2::INVALID_PLATFORM_DESCRIPTOR};
219240
SuspendToken suspend_token{std::noop_coroutine(), WaitEvent::Rechedule{}};
@@ -259,6 +280,7 @@ class wait_for_timer_t : awaitable_impl_::fork_id_watcher_t {
259280

260281
constexpr void await_resume() noexcept {
261282
state = awaitable_impl_::state::end;
283+
async_stack_watcher_t::await_resume();
262284
fork_id_watcher_t::await_resume();
263285
}
264286

@@ -275,7 +297,7 @@ class wait_for_timer_t : awaitable_impl_::fork_id_watcher_t {
275297
// ================================================================================================
276298

277299
template<typename T>
278-
class start_fork_t : awaitable_impl_::fork_id_watcher_t {
300+
class start_fork_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
279301
ForkInstanceState& fork_instance_st{ForkInstanceState::get()};
280302

281303
int64_t fork_id{};
@@ -304,7 +326,8 @@ class start_fork_t : awaitable_impl_::fork_id_watcher_t {
304326
return fork_awaiter.await_ready();
305327
}
306328

307-
std::coroutine_handle<> await_suspend(std::coroutine_handle<> current_coro) noexcept {
329+
template<typename promise_t>
330+
std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_t> current_coro) noexcept {
308331
state = awaitable_impl_::state::suspend;
309332
fork_instance_st.current_id = fork_id;
310333
if (fork_awaiter.await_suspend(current_coro)) [[unlikely]] {
@@ -318,6 +341,7 @@ class start_fork_t : awaitable_impl_::fork_id_watcher_t {
318341

319342
int64_t await_resume() noexcept {
320343
state = awaitable_impl_::state::end;
344+
async_stack_watcher_t::await_resume();
321345
fork_id_watcher_t::await_resume();
322346
fork_awaiter.await_resume();
323347
return fork_id;
@@ -330,7 +354,7 @@ class start_fork_t : awaitable_impl_::fork_id_watcher_t {
330354
// The main difference between them is that wait_fork_t can use shared_task::when_ready as an awaiter,
331355
// whereas wait_fork_result_t can use shared_task::operator co_await.
332356
template<typename T>
333-
class wait_fork_t : awaitable_impl_::fork_id_watcher_t {
357+
class wait_fork_t : awaitable_impl_::fork_id_watcher_t, awaitable_impl_::async_stack_watcher_t {
334358
kphp::coro::shared_task<T> fork_task;
335359
std::remove_cvref_t<decltype(std::declval<kphp::coro::shared_task<T>>().operator co_await())> fork_awaiter;
336360
awaitable_impl_::state state{awaitable_impl_::state::init};
@@ -364,13 +388,15 @@ class wait_fork_t : awaitable_impl_::fork_id_watcher_t {
364388
return state == awaitable_impl_::state::ready;
365389
}
366390

367-
constexpr bool await_suspend(std::coroutine_handle<> coro) noexcept {
391+
template<typename promise_t>
392+
constexpr bool await_suspend(std::coroutine_handle<promise_t> coro) noexcept {
368393
state = awaitable_impl_::state::suspend;
369394
return fork_awaiter.await_suspend(coro);
370395
}
371396

372397
await_resume_t await_resume() noexcept {
373398
state = awaitable_impl_::state::end;
399+
async_stack_watcher_t::await_resume();
374400
fork_id_watcher_t::await_resume();
375401
if constexpr (std::is_void_v<await_resume_t>) {
376402
fork_awaiter.await_resume();
@@ -431,7 +457,8 @@ class wait_with_timeout_t {
431457
// 3. await_suspend returns std::coroutine_handle<>.
432458
// we must guarantee that 'co_await wait_with_timeout_t{awaitable, timeout}' behaves like 'co_await awaitable' except
433459
// it may cancel 'co_await awaitable' if the timeout has elapsed.
434-
await_suspend_return_t await_suspend(std::coroutine_handle<> coro) noexcept {
460+
template<typename promise_t>
461+
await_suspend_return_t await_suspend(std::coroutine_handle<promise_t> coro) noexcept {
435462
// as we don't rely on coroutine scheduler implementation, let's always suspend awaitable first. in case of some smart scheduler
436463
// it won't have any effect, but it will have an effect if our scheduler is quite simple.
437464
state = awaitable_impl_::state::suspend;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2025 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#include "runtime-light/coroutine/coroutine-state.h"
6+
7+
#include "runtime-light/state/instance-state.h"
8+
9+
CoroutineInstanceState& CoroutineInstanceState::get() noexcept {
10+
return InstanceState::get().coroutine_instance_state;
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Compiler for PHP (aka KPHP)
2+
// Copyright (c) 2025 LLC «V Kontakte»
3+
// Distributed under the GPL v3 License, see LICENSE.notice.txt
4+
5+
#pragma once
6+
7+
#include "common/mixin/not_copyable.h"
8+
9+
#include "runtime-light/coroutine/async-stack.h"
10+
11+
struct CoroutineInstanceState final : private vk::not_copyable {
12+
13+
CoroutineInstanceState() noexcept = default;
14+
15+
static CoroutineInstanceState& get() noexcept;
16+
17+
kphp::coro::async_stack_root coroutine_stack_root;
18+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
prepend(RUNTIME_LIGHT_COROUTINE_SRC coroutine/ coroutine-state.cpp)

runtime-light/coroutine/shared-task.h

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <type_traits>
1414
#include <utility>
1515

16+
#include "runtime-light/coroutine/async-stack.h"
1617
#include "runtime-light/k2-platform/k2-api.h"
1718
#include "runtime-light/utils/logs.h"
1819

@@ -27,7 +28,7 @@ struct shared_task_waiter final {
2728
};
2829

2930
template<typename promise_type>
30-
struct promise_base {
31+
struct promise_base : async_stack_element {
3132
constexpr auto initial_suspend() const noexcept -> std::suspend_always {
3233
return {};
3334
}
@@ -50,7 +51,8 @@ struct promise_base {
5051
// read the m_next pointer before resuming the coroutine
5152
// since resuming the coroutine may destroy the shared_task_waiter value
5253
auto* next{waiter->m_next};
53-
waiter->m_continuation.resume();
54+
auto& async_stack_root{*promise.get_async_stack_frame().async_stack_root};
55+
kphp::coro::resume(waiter->m_continuation, async_stack_root);
5456
waiter = next;
5557
}
5658
// return last waiter's coroutine_handle to allow it to potentially be compiled as a tail-call
@@ -96,7 +98,9 @@ struct promise_base {
9698
// start the coroutine if not yet started
9799
if (m_waiters == NOT_STARTED_VAL) {
98100
m_waiters = STARTED_NO_WAITERS_VAL;
99-
std::coroutine_handle<promise_type>::from_promise(*static_cast<promise_type*>(this)).resume();
101+
const auto& handle{std::coroutine_handle<promise_type>::from_promise(*static_cast<promise_type*>(this))};
102+
auto& async_stack_root{*get_async_stack_frame().async_stack_root};
103+
kphp::coro::resume(handle, async_stack_root);
100104
}
101105
// coroutine already completed, don't suspend
102106
if (done()) {
@@ -167,6 +171,23 @@ class awaiter_base {
167171
enum class state : uint8_t { init, suspend, end };
168172
state m_state{state::init};
169173

174+
void set_async_top_frame(async_stack_frame& caller_frame, void* return_address) noexcept {
175+
/**
176+
* shared_task is the top of the stack for calls from it.
177+
* Therefore, it's awaiter doesn't store caller_frame, but it save `await_suspend()` return address
178+
* */
179+
async_stack_frame& callee_frame{m_coro.promise().get_async_stack_frame()};
180+
181+
callee_frame.return_address = return_address;
182+
auto* async_stack_root{caller_frame.async_stack_root};
183+
callee_frame.async_stack_root = async_stack_root;
184+
async_stack_root->top_async_stack_frame = std::addressof(callee_frame);
185+
}
186+
187+
void reset_async_top_frame(async_stack_frame& caller_frame) noexcept {
188+
caller_frame.async_stack_root->top_async_stack_frame = std::addressof(caller_frame);
189+
}
190+
170191
protected:
171192
std::coroutine_handle<promise_type> m_coro;
172193
shared_task_impl::shared_task_waiter m_waiter{};
@@ -195,10 +216,14 @@ class awaiter_base {
195216
return m_coro.promise().done();
196217
}
197218

198-
auto await_suspend(std::coroutine_handle<> awaiter) noexcept -> bool {
219+
template<typename promise_t>
220+
[[clang::noinline]] auto await_suspend(std::coroutine_handle<promise_t> awaiter) noexcept -> bool {
221+
set_async_top_frame(awaiter.promise().get_async_stack_frame(), STACK_RETURN_ADDRESS);
199222
m_state = state::suspend;
200223
m_waiter.m_continuation = awaiter;
201-
return m_coro.promise().suspend_awaiter(m_waiter);
224+
bool should_be_suspended{m_coro.promise().suspend_awaiter(m_waiter)};
225+
reset_async_top_frame(awaiter.promise().get_async_stack_frame());
226+
return should_be_suspended;
202227
}
203228

204229
auto await_resume() noexcept -> void {

0 commit comments

Comments
 (0)