Skip to content

Commit 43dce92

Browse files
committed
WIP: Fixes for shutting down during async operations
1 parent ea55d25 commit 43dce92

File tree

25 files changed

+414
-227
lines changed

25 files changed

+414
-227
lines changed

Core/AppRuntime/Source/AppRuntime.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ namespace Babylon
1919

2020
AppRuntime::~AppRuntime()
2121
{
22+
// Notify the JsRuntime on the JavaScript thread that the JavaScript
23+
// runtime shutdown sequence has begun. The JsRuntimeScheduler will
24+
// use this signal to gracefully cancel asynchronous operations.
25+
Dispatch([](Napi::Env env)
26+
{
27+
JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env));
28+
});
2229
}
2330

2431
void AppRuntime::Run(Napi::Env env)

Core/AppRuntime/Source/WorkQueue.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ namespace Babylon
1414
Resume();
1515
}
1616

17-
m_cancelSource.cancel();
18-
m_dispatcher.cancelled();
17+
// Dispatch a cancel to signal the Run function to gracefully end.
18+
// It must be dispatched and not canceled directly to ensure that
19+
// existing work is executed and executed in the correct order.
20+
m_dispatcher([this]() {
21+
m_cancelSource.cancel();
22+
});
1923

2024
m_thread.join();
2125
}
@@ -44,6 +48,8 @@ namespace Babylon
4448
m_dispatcher.blocking_tick(m_cancelSource);
4549
}
4650

47-
m_dispatcher.clear();
51+
// There should not be any outstanding work during the shutdown sequence
52+
// which should be the only way exit the while loop above.
53+
assert(m_dispatcher.empty());
4854
}
4955
}

Core/AppRuntime/Source/WorkQueue.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ namespace Babylon
1919
template<typename CallableT>
2020
void Append(CallableT callable)
2121
{
22+
if (m_cancelSource.cancelled())
23+
{
24+
// There is likely a coding error if this exception is thrown.
25+
throw std::runtime_error{"Cannot append to the work queue after it is canceled"};
26+
}
27+
2228
// Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a
2329
// copyable callable if necessary.
2430
if constexpr (std::is_copy_constructible<CallableT>::value)

Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <napi/env.h>
88

9+
#include <bx/allocator.h>
910
#include <bgfx/bgfx.h>
1011
#include <bgfx/platform.h>
1112

@@ -94,7 +95,7 @@ namespace Babylon::Graphics
9495

9596
Update GetUpdate(const char* updateName);
9697

97-
void RequestScreenShot(std::function<void(std::vector<uint8_t>)> callback);
98+
arcana::task<std::vector<uint8_t>, std::exception_ptr> RequestScreenShotAsync();
9899

99100
arcana::task<void, std::exception_ptr> ReadTextureAsync(bgfx::TextureHandle handle, gsl::span<uint8_t> data, uint8_t mipLevel = 0);
100101

@@ -114,12 +115,16 @@ namespace Babylon::Graphics
114115
void RemoveTexture(bgfx::TextureHandle handle);
115116
TextureInfo GetTextureInfo(bgfx::TextureHandle handle);
116117

118+
bx::AllocatorI& Allocator() { return m_allocator; }
119+
117120
private:
118121
friend UpdateToken;
119122

120123
DeviceImpl& m_graphicsImpl;
121124

122125
std::unordered_map<uint16_t, TextureInfo> m_textureHandleToInfo{};
123126
std::mutex m_textureHandleToInfoMutex{};
127+
128+
bx::DefaultAllocator m_allocator{};
124129
};
125130
}

Core/Graphics/Source/DeviceContext.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ namespace Babylon::Graphics
5656
return {m_graphicsImpl.GetSafeTimespanGuarantor(updateName), *this};
5757
}
5858

59-
void DeviceContext::RequestScreenShot(std::function<void(std::vector<uint8_t>)> callback)
59+
arcana::task<std::vector<uint8_t>, std::exception_ptr> DeviceContext::RequestScreenShotAsync()
6060
{
61-
return m_graphicsImpl.RequestScreenShot(std::move(callback));
61+
return m_graphicsImpl.RequestScreenShotAsync();
6262
}
6363

6464
arcana::task<void, std::exception_ptr> DeviceContext::ReadTextureAsync(bgfx::TextureHandle handle, gsl::span<uint8_t> data, uint8_t mipLevel)

Core/Graphics/Source/DeviceImpl.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,16 @@ namespace Babylon::Graphics
271271
m_bgfxCallback.SetDiagnosticOutput(std::move(diagnosticOutput));
272272
}
273273

274-
void DeviceImpl::RequestScreenShot(std::function<void(std::vector<uint8_t>)> callback)
274+
arcana::task<std::vector<uint8_t>, std::exception_ptr> DeviceImpl::RequestScreenShotAsync()
275275
{
276-
m_screenShotCallbacks.push(std::move(callback));
276+
arcana::task_completion_source<std::vector<uint8_t>, std::exception_ptr> taskCompletionSource{};
277+
278+
m_screenShotCallbacks.push([taskCompletionSource](std::vector<uint8_t> bytes) mutable
279+
{
280+
taskCompletionSource.complete(std::move(bytes));
281+
});
282+
283+
return taskCompletionSource.as_task();
277284
}
278285

279286
arcana::task<void, std::exception_ptr> DeviceImpl::ReadTextureAsync(bgfx::TextureHandle handle, gsl::span<uint8_t> data, uint8_t mipLevel)

Core/Graphics/Source/DeviceImpl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ namespace Babylon::Graphics
6666

6767
Update GetUpdate(const char* updateName);
6868

69-
void RequestScreenShot(std::function<void(std::vector<uint8_t>)> callback);
69+
arcana::task<std::vector<uint8_t>, std::exception_ptr> RequestScreenShotAsync();
7070

7171
arcana::task<void, std::exception_ptr> ReadTextureAsync(bgfx::TextureHandle handle, gsl::span<uint8_t> data, uint8_t mipLevel);
7272

Core/JsRuntime/Include/Babylon/JsRuntime.h

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <functional>
66
#include <mutex>
7+
#include <vector>
78

89
namespace Babylon
910
{
@@ -22,31 +23,41 @@ namespace Babylon
2223
}
2324
};
2425

25-
struct InternalState;
26-
friend struct InternalState;
26+
JsRuntime(const JsRuntime&) = delete;
27+
JsRuntime& operator=(const JsRuntime&) = delete;
2728

2829
// Any JavaScript errors that occur will bubble up as a Napi::Error C++ exception.
2930
// JsRuntime expects the provided dispatch function to handle this exception,
3031
// such as with a try/catch and logging the exception message.
3132
using DispatchFunctionT = std::function<void(std::function<void(Napi::Env)>)>;
3233

34+
// Creates the JsRuntime object owned by the JavaScript environment.
3335
// Note: It is the contract of JsRuntime that its dispatch function must be usable
3436
// at the moment of construction. JsRuntime cannot be built with dispatch function
3537
// that captures a reference to a not-yet-completed object that will be completed
3638
// later -- an instance of an inheriting type, for example. The dispatch function
3739
// must be safely callable as soon as it is passed to the JsRuntime constructor.
3840
static JsRuntime& CreateForJavaScript(Napi::Env, DispatchFunctionT);
41+
42+
// Gets the JsRuntime from the given N-API environment.
3943
static JsRuntime& GetFromJavaScript(Napi::Env);
40-
void Dispatch(std::function<void(Napi::Env)>);
4144

42-
protected:
43-
JsRuntime(const JsRuntime&) = delete;
44-
JsRuntime& operator=(const JsRuntime&) = delete;
45+
// Notifies the JsRuntime that the JavaScript environment will begin shutting down.
46+
// Calling this function will signal callbacks registered with RegisterDisposing.
47+
static void NotifyDisposing(JsRuntime&);
48+
49+
// Registers a callback for when the JavaScript environment will begin shutting down.
50+
static void RegisterDisposing(JsRuntime&, std::function<void()>);
51+
52+
// Dispatches work onto the JavaScript thread and provides access to the N-API
53+
// environment.
54+
void Dispatch(std::function<void(Napi::Env)>);
4555

4656
private:
4757
JsRuntime(Napi::Env, DispatchFunctionT);
4858

49-
DispatchFunctionT m_dispatchFunction{};
5059
std::mutex m_mutex{};
60+
DispatchFunctionT m_dispatchFunction{};
61+
std::vector<std::function<void()>> m_disposingCallbacks{};
5162
};
5263
}
Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,130 @@
11
#pragma once
22

33
#include "JsRuntime.h"
4+
#include <arcana/threading/dispatcher.h>
5+
#include <atomic>
6+
#include <cassert>
47

58
namespace Babylon
69
{
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.
1155
class JsRuntimeScheduler
1256
{
1357
public:
1458
explicit JsRuntimeScheduler(JsRuntime& runtime)
15-
: m_runtime{runtime}
59+
: m_runtime{&runtime}
60+
, m_scheduler{*this}
1661
{
62+
JsRuntime::RegisterDisposing(*m_runtime, [this]()
63+
{
64+
m_runtime = nullptr;
65+
});
1766
}
1867

19-
template<typename CallableT>
20-
void operator()(CallableT&& callable) const
68+
~JsRuntimeScheduler()
2169
{
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;
2587
}
2688

2789
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{};
29129
};
30130
}

Core/JsRuntime/Source/JsRuntime.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ namespace Babylon
4040
.Data();
4141
}
4242

43+
void JsRuntime::NotifyDisposing(JsRuntime& runtime)
44+
{
45+
auto callbacks = std::move(runtime.m_disposingCallbacks);
46+
for (const auto& callback : callbacks)
47+
{
48+
callback();
49+
}
50+
}
51+
52+
void JsRuntime::RegisterDisposing(JsRuntime& runtime, std::function<void()> callback)
53+
{
54+
runtime.m_disposingCallbacks.push_back(std::move(callback));
55+
}
56+
4357
void JsRuntime::Dispatch(std::function<void(Napi::Env)> function)
4458
{
4559
std::scoped_lock lock{m_mutex};

0 commit comments

Comments
 (0)