Skip to content

Commit 9043df0

Browse files
committed
src: improve performance of coroutine implementation
1 parent 709124c commit 9043df0

File tree

4 files changed

+159
-84
lines changed

4 files changed

+159
-84
lines changed

src/api/callback.cc

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ using v8::Value;
2222
CallbackScope::CallbackScope(Isolate* isolate,
2323
Local<Object> object,
2424
async_context async_context)
25-
: CallbackScope(Environment::GetCurrent(isolate), object, async_context) {}
25+
: CallbackScope(Environment::GetCurrent(isolate), object, async_context) {}
2626

2727
CallbackScope::CallbackScope(Environment* env,
2828
Local<Object> object,
@@ -52,8 +52,7 @@ CallbackScope::CallbackScope(Environment* env,
5252
}
5353

5454
CallbackScope::~CallbackScope() {
55-
if (try_catch_.HasCaught())
56-
private_->MarkAsFailed();
55+
if (try_catch_.HasCaught()) private_->MarkAsFailed();
5756
delete private_;
5857
}
5958

@@ -86,7 +85,15 @@ InternalCallbackScope::InternalCallbackScope(
8685
} else {
8786
object = std::get<Global<Object>*>(object_arg);
8887
}
89-
std::visit([](auto* ptr) { CHECK_NOT_NULL(ptr); }, object);
88+
// Global<Object>* may be null when no resource object was created
89+
// (e.g., coroutine tasks when async_hooks are not active).
90+
// push_async_context already handles the null case by skipping the
91+
// native_execution_async_resources_ store.
92+
if (auto* gptr = std::get_if<Global<Object>*>(&object)) {
93+
CHECK_IMPLIES(*gptr != nullptr, !(*gptr)->IsEmpty());
94+
} else {
95+
std::visit([](auto* ptr) { CHECK_NOT_NULL(ptr); }, object);
96+
}
9097

9198
env->PushAsyncCallbackScope();
9299

@@ -217,8 +224,7 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
217224
Local<Value> context_frame) {
218225
CHECK(!recv.IsEmpty());
219226
#ifdef DEBUG
220-
for (int i = 0; i < argc; i++)
221-
CHECK(!argv[i].IsEmpty());
227+
for (int i = 0; i < argc; i++) CHECK(!argv[i].IsEmpty());
222228
#endif
223229

224230
Local<Function> hook_cb = env->async_hooks_callback_trampoline();
@@ -231,8 +237,9 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
231237
flags = InternalCallbackScope::kSkipAsyncHooks;
232238
use_async_hooks_trampoline =
233239
async_hooks->fields()[AsyncHooks::kBefore] +
234-
async_hooks->fields()[AsyncHooks::kAfter] +
235-
async_hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource] > 0;
240+
async_hooks->fields()[AsyncHooks::kAfter] +
241+
async_hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource] >
242+
0;
236243
}
237244

238245
InternalCallbackScope scope(

src/coro/README.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ asynchronous libuv operations as sequential C++ code using `co_await`.
55

66
The primary goal is to allow multi-step async operations (such as
77
open + stat + read + close) to be written as straight-line C++ instead of
8-
callback chains, while maintaining full integration with Node.js async_hooks,
8+
callback chains, while maintaining full integration with Node.js async\_hooks,
99
AsyncLocalStorage, microtask draining, and environment lifecycle management.
1010

1111
## File overview
1212

1313
* `uv_task.h` -- `UvTask<T>`: The lightweight, untracked coroutine return type.
1414
No V8 or Node.js dependencies. Suitable for internal C++ coroutines that do
15-
not need async_hooks visibility or task queue draining.
15+
not need async\_hooks visibility or task queue draining.
1616

1717
* `uv_tracked_task.h` -- `UvTrackedTask<T, Name>`: The fully-integrated
1818
coroutine return type. Each resume-to-suspend segment is wrapped in an
@@ -64,8 +64,8 @@ static void Access(const FunctionCallbackInfo<Value>& args) {
6464
6565
### Multi-step operations
6666
67-
Multiple libuv calls within a single coroutine are sequential co_await
68-
expressions. The intermediate steps (between two co_await points) are pure C++
67+
Multiple libuv calls within a single coroutine are sequential co\_await
68+
expressions. The intermediate steps (between two co\_await points) are pure C++
6969
with no V8 overhead:
7070
7171
```cpp
@@ -85,7 +85,7 @@ static coro::UvTrackedTask<void, "COROREADFILE"> ReadFileImpl(
8585

8686
### Coroutine composition
8787

88-
`UvTask<T>` and `UvTrackedTask<T, Name>` can be co_awaited from other
88+
`UvTask<T>` and `UvTrackedTask<T, Name>` can be co\_awaited from other
8989
coroutines. This allows factoring common operations into reusable helpers:
9090

9191
```cpp
@@ -104,7 +104,7 @@ UvTrackedTask<void, "MYOP"> OuterCoroutine(Environment* env, ...) {
104104
### UvTask (untracked)
105105
106106
`UvTask<T>` uses lazy initialization. The coroutine does not run until it is
107-
either co_awaited from another coroutine (symmetric transfer) or explicitly
107+
either co\_awaited from another coroutine (symmetric transfer) or explicitly
108108
started with `Start()`. When `Start()` is called, the coroutine runs until its
109109
first `co_await`, then control returns to the caller. The coroutine frame
110110
self-destructs when the coroutine completes.
@@ -119,20 +119,20 @@ adds three phases around `Start()`:
119119
120120
2. **`InitTracking(env)`**: Assigns an `async_id`, captures the current
121121
`async_context_frame` (for AsyncLocalStorage propagation), creates a
122-
resource object for `executionAsyncResource()`, emits the async_hooks
122+
resource object for `executionAsyncResource()`, emits the async\_hooks
123123
`init` event and a trace event, registers in the Environment's coroutine
124124
task list, and reports external memory to V8.
125125
126126
3. **`Start()`**: Marks the task as detached (fire-and-forget) and resumes
127127
the coroutine. Each resume-to-suspend segment is wrapped in an
128128
`InternalCallbackScope` that provides:
129-
* async_hooks `before`/`after` events
129+
* async\_hooks `before`/`after` events
130130
* `async_context_frame` save/restore (AsyncLocalStorage)
131131
* Microtask and `process.nextTick` draining on close
132132
* `request_waiting_` counter management for event loop liveness
133133
134134
4. **Completion**: At `final_suspend`, the last `InternalCallbackScope` is
135-
closed (draining task queues), the async_hooks `destroy` event is emitted,
135+
closed (draining task queues), the async\_hooks `destroy` event is emitted,
136136
the task is unregistered from the Environment, external memory accounting
137137
is released, and the coroutine frame is freed.
138138
@@ -170,22 +170,22 @@ coroutine I/O to finish before destroying the Environment.
170170
171171
For a single async operation (e.g., `fsPromises.access`):
172172
173-
| | ReqWrap pattern | Coroutine pattern |
174-
|---|---|---|
175-
| C++ heap allocations | 3 | 1 (coroutine frame) |
176-
| V8 heap objects | 7 | 3 (resource + resolver + promise) |
177-
| Total allocations | 10 | 4 |
173+
| | ReqWrap pattern | Coroutine pattern |
174+
| -------------------- | --------------- | --------------------------------- |
175+
| C++ heap allocations | 3 | 1 (coroutine frame) |
176+
| V8 heap objects | 7 | 3 (resource + resolver + promise) |
177+
| Total allocations | 10 | 4 |
178178
179179
For a multi-step operation (open + stat + read + close):
180180
181-
| | 4x ReqWrap | Single coroutine |
182-
|---|---|---|
183-
| C++ heap allocations | 12 | 1 |
184-
| V8 heap objects | 28 | 3 |
185-
| Total allocations | 40 | 4 |
186-
| InternalCallbackScope entries | 4 | 5 (one per segment + initial) |
181+
| | 4x ReqWrap | Single coroutine |
182+
| ----------------------------- | ---------- | ----------------------------- |
183+
| C++ heap allocations | 12 | 1 |
184+
| V8 heap objects | 28 | 3 |
185+
| Total allocations | 40 | 4 |
186+
| InternalCallbackScope entries | 4 | 5 (one per segment + initial) |
187187
188-
The coroutine frame embeds the `uv_fs_t` (~440 bytes) directly. The compiler
188+
The coroutine frame embeds the `uv_fs_t` (\~440 bytes) directly. The compiler
189189
may overlay non-simultaneously-live awaitables in the frame, so a multi-step
190190
coroutine does not necessarily pay N times the `uv_fs_t` cost.
191191

0 commit comments

Comments
 (0)