|
1 | 1 | #pragma once |
2 | 2 |
|
3 | 3 | #include "JsRuntime.h" |
| 4 | +#include <arcana/threading/dispatcher.h> |
| 5 | +#include <atomic> |
| 6 | +#include <cassert> |
4 | 7 |
|
5 | 8 | namespace Babylon |
6 | 9 | { |
7 | | - /** |
8 | | - * Scheduler that invokes continuations via JsRuntime::Dispatch. |
9 | | - * Intended to be consumed by arcana.cpp tasks. |
10 | | - */ |
| 10 | + // This class encapsulates a coding pattern for invoking continuations on the JavaScript thread while properly |
| 11 | + // handling garbage collection and shutdown scenarios. This class provides and manages the schedulers intended |
| 12 | + // for a N-API object to use with arcana tasks. It is different than the typical scheduler as this class itself |
| 13 | + // is not a scheduler directly, but instead hands out scheduler via its `Get()` function. It provides special |
| 14 | + // handling for when the JsRuntime begins shutting down, i.e., when JsRuntime::NotifyDisposing is called: |
| 15 | + // 1. The destructor blocks if there are outstanding schedulers not yet invoked on the JavaScript thread. |
| 16 | + // 2. Once the JsRuntime begins shutting down, all schedulers will reroute its dispatch calls from the |
| 17 | + // JsRuntime to a separate dispatcher owned by the JsRuntimeScheduler itself. This class will then be able |
| 18 | + // to pump this dispatcher in its destructor to prevent deadlocks. |
| 19 | + // |
| 20 | + // The typical pattern for an arcana task will look something like this: |
| 21 | + // |
| 22 | + // class MyClass |
| 23 | + // { |
| 24 | + // public: |
| 25 | + // void MyFunction(const Napi::CallbackInfo& info); |
| 26 | + // |
| 27 | + // private: |
| 28 | + // arcana::cancellation_source m_cancellationSource; |
| 29 | + // |
| 30 | + // // Put this last so that it gets destructed first. |
| 31 | + // JsRuntimeScheduler m_runtimeScheduler; |
| 32 | + // }; |
| 33 | + // |
| 34 | + // void MyClass::MyFunction(const Napi::CallbackInfo& info) |
| 35 | + // { |
| 36 | + // const auto callback{info[0].As<Napi::Function>()}; |
| 37 | + // |
| 38 | + // arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, []() |
| 39 | + // { |
| 40 | + // // do some asynchronous work |
| 41 | + // }).then(m_runtimeScheduler.Get(), m_cancelSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)]() { |
| 42 | + // { |
| 43 | + // callback.Call({}); |
| 44 | + // }); |
| 45 | + // } |
| 46 | + // |
| 47 | + // **IMPORTANT**: |
| 48 | + // 1. To prevent continuations from accessing destructed objects, declare the JsRuntimeScheduler at the end of |
| 49 | + // the N-API class. The destructor of the JsRuntimeScheduler will call `Rundown()` which will block until |
| 50 | + // all of its schedulers are invoked. If this is not possible, call `Rundown()` manually in the destructor |
| 51 | + // of the N-API class. |
| 52 | + // 2. The last continuation that accesses members of the N-API object, including the cancellation associated with |
| 53 | + // the continuation must capture a persistent reference to the N-API object itself to prevent GC from collecting |
| 54 | + // the N-API object during the asynchronous operation. |
11 | 55 | class JsRuntimeScheduler |
12 | 56 | { |
13 | 57 | public: |
14 | 58 | explicit JsRuntimeScheduler(JsRuntime& runtime) |
15 | | - : m_runtime{runtime} |
| 59 | + : m_runtime{&runtime} |
| 60 | + , m_scheduler{*this} |
16 | 61 | { |
| 62 | + JsRuntime::RegisterDisposing(*m_runtime, [this]() |
| 63 | + { |
| 64 | + m_runtime = nullptr; |
| 65 | + }); |
17 | 66 | } |
18 | 67 |
|
19 | | - template<typename CallableT> |
20 | | - void operator()(CallableT&& callable) const |
| 68 | + ~JsRuntimeScheduler() |
21 | 69 | { |
22 | | - m_runtime.Dispatch([callable{std::forward<CallableT>(callable)}](Napi::Env){ |
23 | | - callable(); |
24 | | - }); |
| 70 | + Rundown(); |
| 71 | + } |
| 72 | + |
| 73 | + // Wait until all of the schedulers are invoked. |
| 74 | + void Rundown() |
| 75 | + { |
| 76 | + while (m_count > 0) |
| 77 | + { |
| 78 | + m_disposingDispatcher.blocking_tick(arcana::cancellation::none()); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + // Get a scheduler to invoke continuations on the JavaScript thread. |
| 83 | + const auto& Get() |
| 84 | + { |
| 85 | + ++m_count; |
| 86 | + return m_scheduler; |
25 | 87 | } |
26 | 88 |
|
27 | 89 | private: |
28 | | - JsRuntime& m_runtime; |
| 90 | + class SchedulerImpl |
| 91 | + { |
| 92 | + public: |
| 93 | + explicit SchedulerImpl(JsRuntimeScheduler& parent) : m_parent{parent} |
| 94 | + { |
| 95 | + } |
| 96 | + |
| 97 | + template<typename CallableT> |
| 98 | + void operator()(CallableT&& callable) const |
| 99 | + { |
| 100 | + m_parent.Dispatch(callable); |
| 101 | + } |
| 102 | + |
| 103 | + private: |
| 104 | + JsRuntimeScheduler& m_parent; |
| 105 | + }; |
| 106 | + |
| 107 | + template<typename CallableT> |
| 108 | + void Dispatch(CallableT&& callable) |
| 109 | + { |
| 110 | + if (m_runtime != nullptr) |
| 111 | + { |
| 112 | + m_runtime->Dispatch([callable{std::forward<CallableT>(callable)}](Napi::Env) |
| 113 | + { |
| 114 | + callable(); |
| 115 | + }); |
| 116 | + } |
| 117 | + else |
| 118 | + { |
| 119 | + m_disposingDispatcher(callable); |
| 120 | + } |
| 121 | + |
| 122 | + --m_count; |
| 123 | + } |
| 124 | + |
| 125 | + JsRuntime* m_runtime; |
| 126 | + SchedulerImpl m_scheduler; |
| 127 | + std::atomic<int> m_count{0}; |
| 128 | + arcana::manual_dispatcher<128> m_disposingDispatcher{}; |
29 | 129 | }; |
30 | 130 | } |
0 commit comments