1 /**
2 * Copyright (c) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include <algorithm>
17 #include "runtime/coroutines/coroutine.h"
18 #include "runtime/coroutines/stackful_coroutine.h"
19 #include "runtime/include/thread_scopes.h"
20 #include "libpandabase/macros.h"
21 #include "runtime/include/runtime.h"
22 #include "runtime/include/runtime_notification.h"
23 #include "runtime/include/panda_vm.h"
24 #include "runtime/coroutines/stackful_coroutine_manager.h"
25
26 namespace panda {
27
AllocCoroutineStack()28 uint8_t *StackfulCoroutineManager::AllocCoroutineStack()
29 {
30 Pool stackPool = PoolManager::GetMmapMemPool()->AllocPool<OSPagesAllocPolicy::NO_POLICY>(
31 coroStackSizeBytes_, SpaceType::SPACE_TYPE_NATIVE_STACKS, AllocatorType::NATIVE_STACKS_ALLOCATOR);
32 return static_cast<uint8_t *>(stackPool.GetMem());
33 }
34
FreeCoroutineStack(uint8_t * stack)35 void StackfulCoroutineManager::FreeCoroutineStack(uint8_t *stack)
36 {
37 if (stack != nullptr) {
38 PoolManager::GetMmapMemPool()->FreePool(stack, coroStackSizeBytes_);
39 }
40 }
41
CreateWorkers(uint32_t howMany,Runtime * runtime,PandaVM * vm)42 void StackfulCoroutineManager::CreateWorkers(uint32_t howMany, Runtime *runtime, PandaVM *vm)
43 {
44 auto allocator = Runtime::GetCurrent()->GetInternalAllocator();
45
46 auto *wMain = allocator->New<StackfulCoroutineWorker>(
47 runtime, vm, this, StackfulCoroutineWorker::ScheduleLoopType::FIBER, "[main] worker 0");
48 workers_.push_back(wMain);
49
50 for (uint32_t i = 1; i < howMany; ++i) {
51 auto *w = allocator->New<StackfulCoroutineWorker>(
52 runtime, vm, this, StackfulCoroutineWorker::ScheduleLoopType::THREAD, "worker " + ToPandaString(i));
53 workers_.push_back(w);
54 }
55
56 auto *mainCo = CreateMainCoroutine(runtime, vm);
57 mainCo->GetContext<StackfulCoroutineContext>()->SetWorker(wMain);
58 Coroutine::SetCurrent(mainCo);
59 activeWorkersCount_ = 1; // 1 is for MAIN
60
61 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::CreateWorkers(): waiting for workers startup";
62 while (activeWorkersCount_ < howMany) {
63 // NOTE(konstanting, #I67QXC): need timed wait?..
64 workersCv_.Wait(&workersLock_);
65 }
66 }
67
OnWorkerShutdown()68 void StackfulCoroutineManager::OnWorkerShutdown()
69 {
70 os::memory::LockHolder lock(workersLock_);
71 --activeWorkersCount_;
72 workersCv_.Signal();
73 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::OnWorkerShutdown(): COMPLETED, workers left = "
74 << activeWorkersCount_;
75 }
76
OnWorkerStartup()77 void StackfulCoroutineManager::OnWorkerStartup()
78 {
79 os::memory::LockHolder lock(workersLock_);
80 ++activeWorkersCount_;
81 workersCv_.Signal();
82 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::OnWorkerStartup(): COMPLETED, active workers = "
83 << activeWorkersCount_;
84 }
85
DisableCoroutineSwitch()86 void StackfulCoroutineManager::DisableCoroutineSwitch()
87 {
88 GetCurrentWorker()->DisableCoroutineSwitch();
89 }
90
EnableCoroutineSwitch()91 void StackfulCoroutineManager::EnableCoroutineSwitch()
92 {
93 GetCurrentWorker()->EnableCoroutineSwitch();
94 }
95
IsCoroutineSwitchDisabled()96 bool StackfulCoroutineManager::IsCoroutineSwitchDisabled()
97 {
98 return GetCurrentWorker()->IsCoroutineSwitchDisabled();
99 }
100
Initialize(CoroutineManagerConfig config,Runtime * runtime,PandaVM * vm)101 void StackfulCoroutineManager::Initialize(CoroutineManagerConfig config, Runtime *runtime, PandaVM *vm)
102 {
103 // set limits
104 coroStackSizeBytes_ = Runtime::GetCurrent()->GetOptions().GetCoroutineStackSizePages() * os::mem::GetPageSize();
105 if (coroStackSizeBytes_ != AlignUp(coroStackSizeBytes_, PANDA_POOL_ALIGNMENT_IN_BYTES)) {
106 size_t alignmentPages = PANDA_POOL_ALIGNMENT_IN_BYTES / os::mem::GetPageSize();
107 LOG(FATAL, COROUTINES) << "Coroutine stack size should be >= " << alignmentPages
108 << " pages and should be aligned to " << alignmentPages << "-page boundary!";
109 }
110 size_t coroStackAreaSizeBytes = Runtime::GetCurrent()->GetOptions().GetCoroutinesStackMemLimit();
111 coroutineCountLimit_ = coroStackAreaSizeBytes / coroStackSizeBytes_;
112 jsMode_ = config.emulateJs;
113
114 // create and activate workers
115 uint32_t targetNumberOfWorkers = (config.workersCount == CoroutineManagerConfig::WORKERS_COUNT_AUTO)
116 ? std::thread::hardware_concurrency()
117 : static_cast<uint32_t>(config.workersCount);
118 if (config.workersCount == CoroutineManagerConfig::WORKERS_COUNT_AUTO) {
119 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager(): AUTO mode selected, will set number of coroutine "
120 "workers to number of CPUs = "
121 << targetNumberOfWorkers;
122 }
123 os::memory::LockHolder lock(workersLock_);
124 CreateWorkers(targetNumberOfWorkers, runtime, vm);
125 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager(): successfully created and activated " << workers_.size()
126 << " coroutine workers";
127 programCompletionEvent_ = Runtime::GetCurrent()->GetInternalAllocator()->New<GenericEvent>();
128 }
129
Finalize()130 void StackfulCoroutineManager::Finalize()
131 {
132 os::memory::LockHolder lock(coroPoolLock_);
133
134 auto allocator = Runtime::GetCurrent()->GetInternalAllocator();
135 allocator->Delete(programCompletionEvent_);
136 for (auto *co : coroutinePool_) {
137 co->DestroyInternalResources();
138 CoroutineManager::DestroyEntrypointfulCoroutine(co);
139 }
140 coroutinePool_.clear();
141 }
142
AddToRegistry(Coroutine * co)143 void StackfulCoroutineManager::AddToRegistry(Coroutine *co)
144 {
145 os::memory::LockHolder lock(coroListLock_);
146 co->GetVM()->GetGC()->OnThreadCreate(co);
147 coroutines_.insert(co);
148 coroutineCount_++;
149 }
150
RemoveFromRegistry(Coroutine * co)151 void StackfulCoroutineManager::RemoveFromRegistry(Coroutine *co)
152 {
153 coroutines_.erase(co);
154 coroutineCount_--;
155 }
156
RegisterCoroutine(Coroutine * co)157 void StackfulCoroutineManager::RegisterCoroutine(Coroutine *co)
158 {
159 AddToRegistry(co);
160 }
161
TerminateCoroutine(Coroutine * co)162 bool StackfulCoroutineManager::TerminateCoroutine(Coroutine *co)
163 {
164 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::TerminateCoroutine() started";
165 co->NativeCodeEnd();
166 co->UpdateStatus(ThreadStatus::TERMINATING);
167
168 {
169 os::memory::LockHolder lList(coroListLock_);
170 RemoveFromRegistry(co);
171 // DestroyInternalResources()/CleanupInternalResources() must be called in one critical section with
172 // RemoveFromRegistry (under core_list_lock_). This functions transfer cards from coro's post_barrier buffer to
173 // UpdateRemsetThread internally. Situation when cards still remain and UpdateRemsetThread cannot visit the
174 // coro (because it is already removed) must be impossible.
175 if (Runtime::GetOptions().IsUseCoroutinePool() && co->HasManagedEntrypoint()) {
176 co->CleanupInternalResources();
177 } else {
178 co->DestroyInternalResources();
179 }
180 co->UpdateStatus(ThreadStatus::FINISHED);
181 }
182 Runtime::GetCurrent()->GetNotificationManager()->ThreadEndEvent(co);
183
184 if (co->HasManagedEntrypoint()) {
185 UnblockWaiters(co->GetCompletionEvent());
186 CheckProgramCompletion();
187 GetCurrentWorker()->RequestFinalization(co);
188 } else if (co->HasNativeEntrypoint()) {
189 GetCurrentWorker()->RequestFinalization(co);
190 } else {
191 // entrypointless and NOT native: e.g. MAIN
192 // (do nothing, as entrypointless coroutines should should be destroyed manually)
193 }
194
195 return false;
196 }
197
GetActiveWorkersCount()198 size_t StackfulCoroutineManager::GetActiveWorkersCount()
199 {
200 os::memory::LockHolder lkWorkers(workersLock_);
201 return activeWorkersCount_;
202 }
203
CheckProgramCompletion()204 void StackfulCoroutineManager::CheckProgramCompletion()
205 {
206 os::memory::LockHolder lkCompletion(programCompletionLock_);
207 size_t activeWorkerCoros = GetActiveWorkersCount();
208 if (coroutineCount_ == 1 + activeWorkerCoros) { // 1 here is for MAIN
209 LOG(DEBUG, COROUTINES)
210 << "StackfulCoroutineManager::CheckProgramCompletion(): all coroutines finished execution!";
211 // programCompletionEvent_ acts as a stackful-friendly cond var
212 programCompletionEvent_->SetHappened();
213 UnblockWaiters(programCompletionEvent_);
214 } else {
215 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::CheckProgramCompletion(): still "
216 << coroutineCount_ - 1 - activeWorkerCoros << " coroutines exist...";
217 }
218 }
219
CreateCoroutineContext(bool coroHasEntrypoint)220 CoroutineContext *StackfulCoroutineManager::CreateCoroutineContext(bool coroHasEntrypoint)
221 {
222 return CreateCoroutineContextImpl(coroHasEntrypoint);
223 }
224
DeleteCoroutineContext(CoroutineContext * ctx)225 void StackfulCoroutineManager::DeleteCoroutineContext(CoroutineContext *ctx)
226 {
227 FreeCoroutineStack(static_cast<StackfulCoroutineContext *>(ctx)->GetStackLoAddrPtr());
228 Runtime::GetCurrent()->GetInternalAllocator()->Delete(ctx);
229 }
230
GetCoroutineCount()231 size_t StackfulCoroutineManager::GetCoroutineCount()
232 {
233 return coroutineCount_;
234 }
235
GetCoroutineCountLimit()236 size_t StackfulCoroutineManager::GetCoroutineCountLimit()
237 {
238 return coroutineCountLimit_;
239 }
240
Launch(CompletionEvent * completionEvent,Method * entrypoint,PandaVector<Value> && arguments,CoroutineAffinity affinity)241 Coroutine *StackfulCoroutineManager::Launch(CompletionEvent *completionEvent, Method *entrypoint,
242 PandaVector<Value> &&arguments, CoroutineAffinity affinity)
243 {
244 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Launch started";
245
246 auto *result = LaunchImpl(completionEvent, entrypoint, std::move(arguments), affinity);
247 if (result == nullptr) {
248 ThrowOutOfMemoryError("Launch failed");
249 }
250
251 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Launch finished";
252 return result;
253 }
254
Await(CoroutineEvent * awaitee)255 void StackfulCoroutineManager::Await(CoroutineEvent *awaitee)
256 {
257 ASSERT(awaitee != nullptr);
258 [[maybe_unused]] auto *waiter = Coroutine::GetCurrent();
259 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Await started by " + waiter->GetName();
260 if (!GetCurrentWorker()->WaitForEvent(awaitee)) {
261 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Await finished (no await happened)";
262 return;
263 }
264 // NB: at this point the awaitee is likely already deleted
265 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Await finished by " + waiter->GetName();
266 }
267
UnblockWaiters(CoroutineEvent * blocker)268 void StackfulCoroutineManager::UnblockWaiters(CoroutineEvent *blocker)
269 {
270 os::memory::LockHolder lkWorkers(workersLock_);
271 ASSERT(blocker != nullptr);
272 #ifndef NDEBUG
273 {
274 os::memory::LockHolder lkBlocker(*blocker);
275 ASSERT(blocker->Happened());
276 }
277 #endif
278
279 for (auto *w : workers_) {
280 w->UnblockWaiters(blocker);
281 }
282 }
283
Schedule()284 void StackfulCoroutineManager::Schedule()
285 {
286 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::Schedule() request from "
287 << Coroutine::GetCurrent()->GetName();
288 GetCurrentWorker()->RequestSchedule();
289 }
290
EnumerateThreadsImpl(const ThreadManager::Callback & cb,unsigned int incMask,unsigned int xorMask) const291 bool StackfulCoroutineManager::EnumerateThreadsImpl(const ThreadManager::Callback &cb, unsigned int incMask,
292 unsigned int xorMask) const
293 {
294 os::memory::LockHolder lock(coroListLock_);
295 for (auto *t : coroutines_) {
296 if (!ApplyCallbackToThread(cb, t, incMask, xorMask)) {
297 return false;
298 }
299 }
300 return true;
301 }
302
SuspendAllThreads()303 void StackfulCoroutineManager::SuspendAllThreads()
304 {
305 os::memory::LockHolder lock(coroListLock_);
306 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::SuspendAllThreads started";
307 for (auto *t : coroutines_) {
308 t->SuspendImpl(true);
309 }
310 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::SuspendAllThreads finished";
311 }
312
ResumeAllThreads()313 void StackfulCoroutineManager::ResumeAllThreads()
314 {
315 os::memory::LockHolder lock(coroListLock_);
316 for (auto *t : coroutines_) {
317 t->ResumeImpl(true);
318 }
319 }
320
IsRunningThreadExist()321 bool StackfulCoroutineManager::IsRunningThreadExist()
322 {
323 UNREACHABLE();
324 // NOTE(konstanting): correct implementation. Which coroutine do we consider running?
325 return false;
326 }
327
WaitForDeregistration()328 void StackfulCoroutineManager::WaitForDeregistration()
329 {
330 MainCoroutineCompleted();
331 }
332
ReuseCoroutineInstance(Coroutine * co,CompletionEvent * completionEvent,Method * entrypoint,PandaVector<Value> && arguments,PandaString name)333 void StackfulCoroutineManager::ReuseCoroutineInstance(Coroutine *co, CompletionEvent *completionEvent,
334 Method *entrypoint, PandaVector<Value> &&arguments,
335 PandaString name)
336 {
337 auto *ctx = co->GetContext<CoroutineContext>();
338 co->ReInitialize(std::move(name), ctx,
339 Coroutine::ManagedEntrypointInfo {completionEvent, entrypoint, std::move(arguments)});
340 }
341
TryGetCoroutineFromPool()342 Coroutine *StackfulCoroutineManager::TryGetCoroutineFromPool()
343 {
344 os::memory::LockHolder lkPool(coroPoolLock_);
345 if (coroutinePool_.empty()) {
346 return nullptr;
347 }
348 Coroutine *co = coroutinePool_.back();
349 coroutinePool_.pop_back();
350 return co;
351 }
352
ChooseWorkerForCoroutine(CoroutineAffinity affinity)353 StackfulCoroutineWorker *StackfulCoroutineManager::ChooseWorkerForCoroutine(CoroutineAffinity affinity)
354 {
355 switch (affinity) {
356 case CoroutineAffinity::SAME_WORKER: {
357 return GetCurrentWorker();
358 }
359 case CoroutineAffinity::NONE:
360 default: {
361 // choosing the least loaded worker
362 os::memory::LockHolder lkWorkers(workersLock_);
363 auto w = std::min_element(workers_.begin(), workers_.end(),
364 [](const StackfulCoroutineWorker *a, const StackfulCoroutineWorker *b) {
365 return a->GetLoadFactor() < b->GetLoadFactor();
366 });
367 return *w;
368 }
369 }
370 }
371
LaunchImpl(CompletionEvent * completionEvent,Method * entrypoint,PandaVector<Value> && arguments,CoroutineAffinity affinity)372 Coroutine *StackfulCoroutineManager::LaunchImpl(CompletionEvent *completionEvent, Method *entrypoint,
373 PandaVector<Value> &&arguments, CoroutineAffinity affinity)
374 {
375 #ifndef NDEBUG
376 GetCurrentWorker()->PrintRunnables("LaunchImpl begin");
377 #endif
378 auto coroName = entrypoint->GetFullName();
379
380 Coroutine *co = nullptr;
381 if (Runtime::GetOptions().IsUseCoroutinePool()) {
382 co = TryGetCoroutineFromPool();
383 }
384 if (co != nullptr) {
385 ReuseCoroutineInstance(co, completionEvent, entrypoint, std::move(arguments), std::move(coroName));
386 } else {
387 co = CreateCoroutineInstance(completionEvent, entrypoint, std::move(arguments), std::move(coroName));
388 }
389 if (co == nullptr) {
390 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::LaunchImpl: failed to create a coroutine!";
391 return co;
392 }
393 Runtime::GetCurrent()->GetNotificationManager()->ThreadStartEvent(co);
394
395 auto *w = ChooseWorkerForCoroutine(affinity);
396 w->AddRunnableCoroutine(co, IsJsMode());
397
398 #ifndef NDEBUG
399 GetCurrentWorker()->PrintRunnables("LaunchImpl end");
400 #endif
401 return co;
402 }
403
MainCoroutineCompleted()404 void StackfulCoroutineManager::MainCoroutineCompleted()
405 {
406 // precondition: MAIN is already in the native mode
407 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::MainCoroutineCompleted(): STARTED";
408
409 // block till only schedule loop coroutines are present
410 LOG(DEBUG, COROUTINES)
411 << "StackfulCoroutineManager::MainCoroutineCompleted(): waiting for other coroutines to complete";
412
413 {
414 os::memory::LockHolder lkCompletion(programCompletionLock_);
415 auto *main = Coroutine::GetCurrent();
416 while (coroutineCount_ > 1 + GetActiveWorkersCount()) { // 1 is for MAIN
417 programCompletionEvent_->SetNotHappened();
418 programCompletionEvent_->Lock();
419 programCompletionLock_.Unlock();
420 ScopedManagedCodeThread s(main); // perf?
421 GetCurrentWorker()->WaitForEvent(programCompletionEvent_);
422 LOG(DEBUG, COROUTINES)
423 << "StackfulCoroutineManager::MainCoroutineCompleted(): possibly spurious wakeup from wait...";
424 // NOTE(konstanting, #I67QXC): test for the spurious wakeup
425 programCompletionLock_.Lock();
426 }
427 ASSERT(coroutineCount_ == (1 + GetActiveWorkersCount()));
428 }
429
430 // NOTE(konstanting, #I67QXC): correct state transitions for MAIN
431 GetCurrentContext()->MainThreadFinished();
432 GetCurrentContext()->EnterAwaitLoop();
433
434 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::MainCoroutineCompleted(): stopping workers";
435 {
436 os::memory::LockHolder lock(workersLock_);
437 for (auto *worker : workers_) {
438 worker->SetActive(false);
439 }
440 while (activeWorkersCount_ > 1) { // 1 is for MAIN
441 // NOTE(konstanting, #I67QXC): need timed wait?..
442 workersCv_.Wait(&workersLock_);
443 }
444 }
445
446 LOG(DEBUG, COROUTINES)
447 << "StackfulCoroutineManager::MainCoroutineCompleted(): stopping await loop on the main worker";
448 while (coroutineCount_ > 1) {
449 GetCurrentWorker()->FinalizeFiberScheduleLoop();
450 }
451
452 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::MainCoroutineCompleted(): deleting workers";
453 {
454 os::memory::LockHolder lkWorkers(workersLock_);
455 for (auto *worker : workers_) {
456 Runtime::GetCurrent()->GetInternalAllocator()->Delete(worker);
457 }
458 }
459
460 LOG(DEBUG, COROUTINES) << "StackfulCoroutineManager::MainCoroutineCompleted(): DONE";
461 }
462
GetCurrentContext()463 StackfulCoroutineContext *StackfulCoroutineManager::GetCurrentContext()
464 {
465 auto *co = Coroutine::GetCurrent();
466 return co->GetContext<StackfulCoroutineContext>();
467 }
468
GetCurrentWorker()469 StackfulCoroutineWorker *StackfulCoroutineManager::GetCurrentWorker()
470 {
471 return GetCurrentContext()->GetWorker();
472 }
473
IsJsMode()474 bool StackfulCoroutineManager::IsJsMode()
475 {
476 return jsMode_;
477 }
478
DestroyEntrypointfulCoroutine(Coroutine * co)479 void StackfulCoroutineManager::DestroyEntrypointfulCoroutine(Coroutine *co)
480 {
481 if (Runtime::GetOptions().IsUseCoroutinePool() && co->HasManagedEntrypoint()) {
482 co->CleanUp();
483 os::memory::LockHolder lock(coroPoolLock_);
484 coroutinePool_.push_back(co);
485 } else {
486 CoroutineManager::DestroyEntrypointfulCoroutine(co);
487 }
488 }
489
CreateCoroutineContextImpl(bool needStack)490 StackfulCoroutineContext *StackfulCoroutineManager::CreateCoroutineContextImpl(bool needStack)
491 {
492 uint8_t *stack = nullptr;
493 size_t stackSizeBytes = 0;
494 if (needStack) {
495 stack = AllocCoroutineStack();
496 if (stack == nullptr) {
497 return nullptr;
498 }
499 stackSizeBytes = coroStackSizeBytes_;
500 }
501 return Runtime::GetCurrent()->GetInternalAllocator()->New<StackfulCoroutineContext>(stack, stackSizeBytes);
502 }
503
CreateNativeCoroutine(Runtime * runtime,PandaVM * vm,Coroutine::NativeEntrypointInfo::NativeEntrypointFunc entry,void * param,PandaString name)504 Coroutine *StackfulCoroutineManager::CreateNativeCoroutine(Runtime *runtime, PandaVM *vm,
505 Coroutine::NativeEntrypointInfo::NativeEntrypointFunc entry,
506 void *param, PandaString name)
507 {
508 if (GetCoroutineCount() >= GetCoroutineCountLimit()) {
509 // resource limit reached
510 return nullptr;
511 }
512 StackfulCoroutineContext *ctx = CreateCoroutineContextImpl(true);
513 if (ctx == nullptr) {
514 // do not proceed if we cannot create a context for the new coroutine
515 return nullptr;
516 }
517 auto *co = GetCoroutineFactory()(runtime, vm, std::move(name), ctx, Coroutine::NativeEntrypointInfo(entry, param));
518 ASSERT(co != nullptr);
519
520 // Let's assume that even the "native" coroutine can eventually try to execute some managed code.
521 // In that case pre/post barrier buffers are necessary.
522 co->InitBuffers();
523 return co;
524 }
525
DestroyNativeCoroutine(Coroutine * co)526 void StackfulCoroutineManager::DestroyNativeCoroutine(Coroutine *co)
527 {
528 DestroyEntrypointlessCoroutine(co);
529 }
530
531 } // namespace panda
532