Add error handling to RuntimeScheduler

Summary:
changelog: [internal]

Catch JavaScript errors and forward them to `ErrorUtils` in *RuntimeScheduler*. This makes sure that JS errors are handled by ErrorUtils and do not bubble up to bridge.

Reviewed By: philIip

Differential Revision: D31429001

fbshipit-source-id: 50f865872e4cd3ba180056099ff40f5962ee7a77
This commit is contained in:
Samuel Susla
2021-10-07 15:23:11 -07:00
committed by Facebook GitHub Bot
parent e612d3a116
commit 27304fcd0b
4 changed files with 156 additions and 17 deletions
@@ -0,0 +1,36 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <jsi/jsi.h>
namespace facebook {
namespace react {
inline static void handleFatalError(
jsi::Runtime &runtime,
const jsi::JSError &error) {
auto reportFatalError = "reportFatalError";
auto errorUtils = runtime.global().getProperty(runtime, "ErrorUtils");
if (errorUtils.isUndefined() || !errorUtils.isObject() ||
!errorUtils.getObject(runtime).hasProperty(runtime, reportFatalError)) {
// ErrorUtils was not set up. This probably means the bundle didn't
// load properly.
throw jsi::JSError(
runtime,
"ErrorUtils is not set up properly. Something probably went wrong trying to load the JS bundle. Trying to report error " +
error.getMessage(),
error.getStack());
}
auto func = errorUtils.asObject(runtime).getPropertyAsFunction(
runtime, reportFatalError);
func.call(runtime, error.value());
}
} // namespace react
} // namespace facebook
@@ -6,6 +6,7 @@
*/
#include "RuntimeScheduler.h"
#include "ErrorUtils.h"
namespace facebook {
namespace react {
@@ -89,27 +90,32 @@ void RuntimeScheduler::executeNowOnTheSameThread(
void RuntimeScheduler::startWorkLoop(jsi::Runtime &runtime) const {
auto previousPriority = currentPriority_;
isPerformingWork_ = true;
while (!taskQueue_.empty()) {
auto topPriorityTask = taskQueue_.top();
auto now = now_();
auto didUserCallbackTimeout = topPriorityTask->expirationTime <= now;
try {
while (!taskQueue_.empty()) {
auto topPriorityTask = taskQueue_.top();
auto now = now_();
auto didUserCallbackTimeout = topPriorityTask->expirationTime <= now;
if (!didUserCallbackTimeout && shouldYield_) {
// This task hasn't expired and we need to yield.
break;
}
currentPriority_ = topPriorityTask->priority;
auto result = topPriorityTask->execute(runtime);
if (!didUserCallbackTimeout && shouldYield_) {
// This task hasn't expired and we need to yield.
break;
}
currentPriority_ = topPriorityTask->priority;
auto result = topPriorityTask->execute(runtime);
if (result.isObject() && result.getObject(runtime).isFunction(runtime)) {
topPriorityTask->callback =
result.getObject(runtime).getFunction(runtime);
} else {
if (taskQueue_.top() == topPriorityTask) {
taskQueue_.pop();
if (result.isObject() && result.getObject(runtime).isFunction(runtime)) {
topPriorityTask->callback =
result.getObject(runtime).getFunction(runtime);
} else {
if (taskQueue_.top() == topPriorityTask) {
taskQueue_.pop();
}
}
}
} catch (jsi::JSError &error) {
handleFatalError(runtime, error);
}
currentPriority_ = previousPriority;
isPerformingWork_ = false;
}
@@ -10,7 +10,9 @@
#include <jsi/jsi.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <memory>
#include "StubClock.h"
#include "StubErrorUtils.h"
#include "StubQueue.h"
namespace facebook::react {
@@ -22,6 +24,7 @@ class RuntimeSchedulerTest : public testing::Test {
void SetUp() override {
hostFunctionCallCount_ = 0;
runtime_ = facebook::hermes::makeHermesRuntime();
stubErrorUtils_ = StubErrorUtils::createAndInstallIfNeeded(*runtime_);
stubQueue_ = std::make_unique<StubQueue>();
RuntimeExecutor runtimeExecutor =
@@ -52,7 +55,7 @@ class RuntimeSchedulerTest : public testing::Test {
jsi::Runtime &,
jsi::Value const &,
jsi::Value const *arguments,
size_t) noexcept -> jsi::Value {
size_t) -> jsi::Value {
++hostFunctionCallCount_;
auto didUserCallbackTimeout = arguments[0].getBool();
return callback(didUserCallbackTimeout);
@@ -65,6 +68,7 @@ class RuntimeSchedulerTest : public testing::Test {
std::unique_ptr<StubClock> stubClock_;
std::unique_ptr<StubQueue> stubQueue_;
std::unique_ptr<RuntimeScheduler> runtimeScheduler_;
std::shared_ptr<StubErrorUtils> stubErrorUtils_;
};
TEST_F(RuntimeSchedulerTest, now) {
@@ -469,4 +473,25 @@ TEST_F(RuntimeSchedulerTest, scheduleTaskFromTask) {
EXPECT_EQ(stubQueue_->size(), 0);
}
TEST_F(RuntimeSchedulerTest, handlingError) {
bool didRunTask = false;
auto firstCallback = createHostFunctionFromLambda([this, &didRunTask](bool) {
didRunTask = true;
jsi::detail::throwJSError(*runtime_, "Test error");
return jsi::Value::undefined();
});
runtimeScheduler_->scheduleTask(
SchedulerPriority::NormalPriority, std::move(firstCallback));
EXPECT_FALSE(didRunTask);
EXPECT_EQ(stubQueue_->size(), 1);
stubQueue_->tick();
EXPECT_TRUE(didRunTask);
EXPECT_EQ(stubQueue_->size(), 0);
EXPECT_EQ(stubErrorUtils_->getReportFatalCallCount(), 1);
}
} // namespace facebook::react
@@ -0,0 +1,72 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <glog/logging.h>
#include <jsi/jsi.h>
namespace facebook {
namespace react {
/*
* Exposes StubErrorUtils to JavaScript realm.
*/
class StubErrorUtils : public jsi::HostObject {
public:
static std::shared_ptr<StubErrorUtils> createAndInstallIfNeeded(
jsi::Runtime &runtime) {
auto errorUtilsModuleName = "ErrorUtils";
auto errorUtilsValue =
runtime.global().getProperty(runtime, errorUtilsModuleName);
if (errorUtilsValue.isUndefined()) {
auto stubErrorUtils = std::make_shared<StubErrorUtils>();
auto object = jsi::Object::createFromHostObject(runtime, stubErrorUtils);
runtime.global().setProperty(
runtime, errorUtilsModuleName, std::move(object));
return stubErrorUtils;
}
auto stubErrorUtilsObject = errorUtilsValue.asObject(runtime);
return stubErrorUtilsObject.getHostObject<StubErrorUtils>(runtime);
}
/*
* `jsi::HostObject` specific overloads.
*/
jsi::Value get(jsi::Runtime &runtime, jsi::PropNameID const &name) override {
auto propertyName = name.utf8(runtime);
if (propertyName == "reportFatalError") {
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[this](
jsi::Runtime &runtime,
jsi::Value const &,
jsi::Value const *arguments,
size_t) noexcept -> jsi::Value {
reportFatalCallCount_++;
return jsi::Value::undefined();
});
}
return jsi::Value::undefined();
}
int getReportFatalCallCount() const {
return reportFatalCallCount_;
}
private:
int reportFatalCallCount_;
};
} // namespace react
} // namespace facebook