Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for shutting down during async operations #1141

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
43dce92
WIP: Fixes for shutting down during async operations
bghgary Sep 29, 2022
925d243
Change to explicitly call Rundown
bghgary Sep 29, 2022
c999153
Cannot throw in destructors
bghgary Sep 30, 2022
204efb9
Merge remote-tracking branch 'origin/master' into async-fixes
bghgary Oct 26, 2022
9d1e098
Fix merge issues
bghgary Oct 26, 2022
124def4
Merge with new timeout code with work queue fixes
bghgary Oct 27, 2022
309c4d9
Fix Canvas
bghgary Oct 28, 2022
ad88d30
Fix Android build
bghgary Oct 28, 2022
11ca510
Merge remote-tracking branch 'origin/master' into async-fixes
bghgary Mar 29, 2023
31248b6
Merge branch 'async-fixes' of https://github.com/bghgary/BabylonNativ…
bghgary Mar 29, 2023
9322360
Update arcana.cpp
bghgary Apr 4, 2023
b4a3c73
Temp fixes for MediaStream
bghgary Apr 5, 2023
9305ca8
Add missing std::forward calls for perfect forwarding
bghgary Apr 19, 2023
df246e2
Work queue shutdown fixes
bghgary Apr 28, 2023
7350715
Miscellaneous Windows AppRuntime fixes
bghgary Apr 28, 2023
77dc8f3
Fix typo in AppRuntime for Win32
bghgary Apr 28, 2023
def047c
Better fix for work queue shutdown issue
bghgary May 2, 2023
bdaf1b9
Merge remote-tracking branch 'origin/master' into async-fixes
bghgary May 4, 2023
0ae6cf4
Fix build issues from merge
bghgary May 4, 2023
71efaf4
Update arcana.cpp to include continuation fix
bghgary May 9, 2023
a0db419
Minor style fixes
bghgary May 9, 2023
6909f55
Update comment
bghgary May 9, 2023
e9d229d
Update comment 2
bghgary May 9, 2023
423ed5e
More style fixes
bghgary May 9, 2023
36a4661
Merge AppRuntime and WorkQueue to fix race condition
bghgary May 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Core/AppRuntime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ set(SOURCES
"Include/Babylon/Dispatchable.h"
"Include/Babylon/AppRuntime.h"
"Source/AppRuntime.cpp"
"Source/AppRuntime_${NAPI_JAVASCRIPT_ENGINE}.cpp"
"Source/AppRuntime_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}"
"Source/WorkQueue.cpp"
"Source/WorkQueue.h")
"Source/AppRuntimeImpl.h"
"Source/AppRuntimeImpl.cpp"
"Source/AppRuntimeImpl_${NAPI_JAVASCRIPT_ENGINE}.cpp"
"Source/AppRuntimeImpl_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}")

add_library(AppRuntime ${SOURCES})
warnings_as_errors(AppRuntime)
Expand Down
32 changes: 8 additions & 24 deletions Core/AppRuntime/Include/Babylon/AppRuntime.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#pragma once

#include <napi/env.h>

#include "Dispatchable.h"
#include <napi/napi.h>

#include <memory>
#include <functional>
#include <exception>

namespace Babylon
{
class WorkQueue;
class AppRuntimeImpl;

class AppRuntime final
{
Expand All @@ -18,33 +19,16 @@ namespace Babylon
AppRuntime(std::function<void(const std::exception&)> unhandledExceptionHandler);
~AppRuntime();

// Move semantics
AppRuntime(AppRuntime&&) noexcept;
AppRuntime& operator=(AppRuntime&&) noexcept;

void Suspend();
void Resume();

void Dispatch(Dispatchable<void(Napi::Env)> callback);

private:
// These three methods are the mechanism by which platform- and JavaScript-specific
// code can be "injected" into the execution of the JavaScript thread. These three
// functions are implemented in separate files, thus allowing implementations to be
// mixed and matched by the build system based on the platform and JavaScript engine
// being targeted, without resorting to virtuality. An important nuance of these
// functions is that they are all intended to call each other: RunPlatformTier MUST
// call RunEnvironmentTier, which MUST create the initial Napi::Env and pass it to
// Run. This arrangement allows not only for an arbitrary assemblage of platforms,
// but it also allows us to respect the requirement by certain platforms (notably V8)
// that certain program state be allocated and stored only on the stack.
void RunPlatformTier();
void RunEnvironmentTier(const char* executablePath = ".");
void Run(Napi::Env);

// This method is called from Dispatch to allow platform-specific code to add
// extra logic around the invocation of a dispatched callback.
void Execute(Dispatchable<void()> callback);

static void DefaultUnhandledExceptionHandler(const std::exception& error);

std::unique_ptr<WorkQueue> m_workQueue{};
std::function<void(const std::exception&)> m_unhandledExceptionHandler{};
std::unique_ptr<AppRuntimeImpl> m_impl;
};
}
45 changes: 11 additions & 34 deletions Core/AppRuntime/Source/AppRuntime.cpp
Original file line number Diff line number Diff line change
@@ -1,59 +1,36 @@
#include "AppRuntime.h"
#include "WorkQueue.h"
#include <Babylon/JsRuntime.h>
#include "AppRuntimeImpl.h"

namespace Babylon
{
AppRuntime::AppRuntime()
: AppRuntime{DefaultUnhandledExceptionHandler}
: m_impl{std::make_unique<AppRuntimeImpl>()}
{
}

AppRuntime::AppRuntime(std::function<void(const std::exception&)> unhandledExceptionHandler)
: m_workQueue{std::make_unique<WorkQueue>([this] { RunPlatformTier(); })}
, m_unhandledExceptionHandler{unhandledExceptionHandler}
: m_impl{std::make_unique<AppRuntimeImpl>(unhandledExceptionHandler)}
Copy link
Member

@ryantrem ryantrem May 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This this be:
: m_impl{std::make_unique<AppRuntimeImpl>(std::move(unhandledExceptionHandler))}

{
Dispatch([this](Napi::Env env) {
JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); });
});
}

AppRuntime::~AppRuntime()
{
}
AppRuntime::~AppRuntime() = default;

void AppRuntime::Run(Napi::Env env)
{
m_workQueue->Run(env);
}
// Move semantics
AppRuntime::AppRuntime(AppRuntime&&) noexcept = default;
AppRuntime& AppRuntime::operator=(AppRuntime&&) noexcept = default;

void AppRuntime::Suspend()
{
m_workQueue->Suspend();
m_impl->Suspend();
}

void AppRuntime::Resume()
{
m_workQueue->Resume();
m_impl->Resume();
}

void AppRuntime::Dispatch(Dispatchable<void(Napi::Env)> func)
void AppRuntime::Dispatch(Dispatchable<void(Napi::Env)> callback)
{
m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable {
Execute([this, env, func{std::move(func)}]() mutable {
try
{
func(env);
}
catch (const std::exception& error)
{
m_unhandledExceptionHandler(error);
}
catch (...)
{
std::abort();
}
});
});
m_impl->Dispatch(std::move(callback));
}
}
88 changes: 88 additions & 0 deletions Core/AppRuntime/Source/AppRuntimeImpl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "AppRuntimeImpl.h"
#include <Babylon/JsRuntime.h>

namespace Babylon
{
AppRuntimeImpl::AppRuntimeImpl(std::function<void(const std::exception&)> unhandledExceptionHandler)
: m_unhandledExceptionHandler{std::move(unhandledExceptionHandler)}
, m_thread{[this] { RunPlatformTier(); }}
{
Dispatch([this](Napi::Env env) {
JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); });
});
}

AppRuntimeImpl::~AppRuntimeImpl()
{
if (m_suspensionLock.has_value())
{
m_suspensionLock.reset();
}

Dispatch([this](Napi::Env env) {
// Notify the JsRuntime on the JavaScript thread that the JavaScript runtime shutdown sequence has
// begun. The JsRuntimeScheduler will use this signal to gracefully cancel asynchronous operations.
JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env));

// Cancel on the JavaScript thread to signal the Run function to gracefully end. It must be
// dispatched and not canceled directly to ensure that existing work is executed and executed in
// the correct order.
m_cancellationSource.cancel();
});

m_thread.join();
}

void AppRuntimeImpl::Suspend()
{
auto suspensionMutex = std::make_shared<std::mutex>();
m_suspensionLock.emplace(*suspensionMutex);
Append([suspensionMutex = std::move(suspensionMutex)](Napi::Env) {
std::scoped_lock lock{*suspensionMutex};
});
}

void AppRuntimeImpl::Resume()
{
m_suspensionLock.reset();
}

void AppRuntimeImpl::Dispatch(Dispatchable<void(Napi::Env)> func)
{
Append([this, func{std::move(func)}](Napi::Env env) mutable {
Execute([this, env, func{std::move(func)}]() mutable {
try
{
func(env);
}
catch (const std::exception& error)
{
m_unhandledExceptionHandler(error);
}
catch (...)
{
std::abort();
}
});
});
}

void AppRuntimeImpl::Run(Napi::Env env)
{
m_env = std::make_optional(env);

m_dispatcher.set_affinity(std::this_thread::get_id());

while (!m_cancellationSource.cancelled())
{
m_dispatcher.blocking_tick(m_cancellationSource);
}

// The dispatcher can be non-empty if something is dispatched after cancellation.
// For example, Chakra's JsSetPromiseContinuationCallback may potentially dispatch
// a continuation after cancellation.
m_dispatcher.clear();

m_env.reset();
}
}
68 changes: 68 additions & 0 deletions Core/AppRuntime/Source/AppRuntimeImpl.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

#include "AppRuntime.h"
#include <optional>
#include <mutex>
#include <arcana/threading/dispatcher.h>

namespace Babylon
{
class AppRuntimeImpl
{
public:
AppRuntimeImpl(std::function<void(const std::exception&)> unhandledExceptionHandler = DefaultUnhandledExceptionHandler);
~AppRuntimeImpl();

void Suspend();
void Resume();

void Dispatch(Dispatchable<void(Napi::Env)> func);

private:
static void DefaultUnhandledExceptionHandler(const std::exception& error);

template<typename CallableT>
void Append(CallableT callable)
{
// Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a
// copyable callable if necessary.
if constexpr (std::is_copy_constructible<CallableT>::value)
{
m_dispatcher.queue([this, callable = std::move(callable)]() {
callable(m_env.value());
});
}
else
{
m_dispatcher.queue([this, callablePtr = std::make_shared<CallableT>(std::move(callable))]() {
(*callablePtr)(m_env.value());
});
}
}

// These three methods are the mechanism by which platform- and JavaScript-specific
// code can be "injected" into the execution of the JavaScript thread. These three
// functions are implemented in separate files, thus allowing implementations to be
// mixed and matched by the build system based on the platform and JavaScript engine
// being targeted, without resorting to virtuality. An important nuance of these
// functions is that they are all intended to call each other: RunPlatformTier MUST
// call RunEnvironmentTier, which MUST create the initial Napi::Env and pass it to
// Run. This arrangement allows not only for an arbitrary assemblage of platforms,
// but it also allows us to respect the requirement by certain platforms (notably V8)
// that certain program state be allocated and stored only on the stack.
void RunPlatformTier();
void RunEnvironmentTier(const char* executablePath = ".");
void Run(Napi::Env);

// This method is called from Dispatch to allow platform-specific code to add
// extra logic around the invocation of a dispatched callback.
void Execute(Dispatchable<void()> callback);

std::function<void(const std::exception&)> m_unhandledExceptionHandler{};
std::optional<Napi::Env> m_env{};
std::optional<std::scoped_lock<std::mutex>> m_suspensionLock{};
arcana::cancellation_source m_cancellationSource{};
arcana::manual_dispatcher<128> m_dispatcher{};
std::thread m_thread{};
};
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
#include "AppRuntime.h"
#include "AppRuntimeImpl.h"
#include <exception>
#include <sstream>
#include <android/log.h>

namespace Babylon
{
void AppRuntime::RunPlatformTier()
void AppRuntimeImpl::RunPlatformTier()
{
RunEnvironmentTier();
}

void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error)
void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error)
{
std::stringstream ss{};
ss << "Uncaught Error: " << error.what() << std::endl;
__android_log_write(ANDROID_LOG_ERROR, "BabylonNative", ss.str().data());
}

void AppRuntime::Execute(Dispatchable<void()> callback)
void AppRuntimeImpl::Execute(Dispatchable<void()> callback)
{
callback();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "AppRuntime.h"
#include "AppRuntimeImpl.h"

#include <napi/env.h>

Expand All @@ -20,33 +20,23 @@ namespace Babylon
}
}

void AppRuntime::RunEnvironmentTier(const char*)
void AppRuntimeImpl::RunEnvironmentTier(const char*)
{
using DispatchFunction = std::function<void(std::function<void()>)>;
DispatchFunction dispatchFunction =
[this](std::function<void()> action) {
Dispatch([action = std::move(action)](Napi::Env) {
action();
});
};

JsRuntimeHandle jsRuntime;
ThrowIfFailed(JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &jsRuntime));
JsContextRef context;
ThrowIfFailed(JsCreateContext(jsRuntime, &context));
ThrowIfFailed(JsSetCurrentContext(context));
ThrowIfFailed(JsSetPromiseContinuationCallback(
[](JsValueRef task, void* callbackState) {
ThrowIfFailed(JsAddRef(task, nullptr));
auto* dispatch = reinterpret_cast<DispatchFunction*>(callbackState);
dispatch->operator()([task]() {
JsValueRef undefined;
ThrowIfFailed(JsGetUndefinedValue(&undefined));
ThrowIfFailed(JsCallFunction(task, &undefined, 1, nullptr));
ThrowIfFailed(JsRelease(task, nullptr));
});
},
&dispatchFunction));
ThrowIfFailed(JsSetPromiseContinuationCallback([](JsValueRef task, void* callbackState) {
auto* pThis = reinterpret_cast<AppRuntimeImpl*>(callbackState);
ThrowIfFailed(JsAddRef(task, nullptr));
pThis->Dispatch([task](auto) {
JsValueRef global;
ThrowIfFailed(JsGetGlobalObject(&global));
ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr));
ThrowIfFailed(JsRelease(task, nullptr));
});
}, this));
ThrowIfFailed(JsProjectWinRTNamespace(L"Windows"));

#if defined(_DEBUG)
Expand Down
Loading