Skip to content

Commit 55b886a

Browse files
committed
fix(hermes): microtask handling
1 parent 528c72a commit 55b886a

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

test-app/app/src/main/assets/app/MyActivity.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
}
2222
}
2323
*/
24-
const benchmarkRunner = require("./benchmark.js");
2524
var MyActivity = (function (_super) {
2625
__extends(MyActivity, _super);
2726
function MyActivity() {
@@ -68,14 +67,32 @@ var MyActivity = (function (_super) {
6867
},
6968
})
7069
);
70+
var benchmarkWorker = null;
7171
button2.setOnClickListener(
7272
new android.view.View.OnClickListener("AppClickListener", {
7373
onClick: function () {
74-
const result = benchmarkRunner.runBenchmark();
75-
setTimeout(() => {
76-
globalThis.gc();
77-
});
78-
textView.setText(result);
74+
// Run the benchmark on a worker thread so the UI thread stays
75+
// responsive and Android does not raise an ANR.
76+
if (benchmarkWorker) {
77+
return;
78+
}
79+
button2.setText("Running Benchmark...");
80+
textView.setText("Running benchmark, please wait...");
81+
82+
benchmarkWorker = new Worker("./benchmark-worker.js");
83+
benchmarkWorker.onmessage = function (msg) {
84+
textView.setText(msg.data);
85+
button2.setText("Run Benchmark");
86+
benchmarkWorker.terminate();
87+
benchmarkWorker = null;
88+
};
89+
benchmarkWorker.onerror = function (err) {
90+
textView.setText("Benchmark error: " + (err && err.message ? err.message : err));
91+
button2.setText("Run Benchmark");
92+
benchmarkWorker.terminate();
93+
benchmarkWorker = null;
94+
};
95+
benchmarkWorker.postMessage("start");
7996
},
8097
})
8198
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Runs the Octane benchmark suite off the main (UI) thread so the app
2+
// stays responsive and Android does not raise an ANR while benchmarking.
3+
const benchmarkRunner = require("./benchmark.js");
4+
5+
self.onmessage = function () {
6+
try {
7+
const result = benchmarkRunner.runBenchmark();
8+
if (typeof globalThis.gc === "function") {
9+
globalThis.gc();
10+
}
11+
self.postMessage(result);
12+
} catch (e) {
13+
self.postMessage("Benchmark failed: " + (e && e.stack ? e.stack : e));
14+
}
15+
};

test-app/runtime/src/main/cpp/napi/hermes/jsr.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ JSR::JSR() {
1313
.withMicrotaskQueue(true)
1414
.withES6BlockScoping(true)
1515
.withEnableAsyncGenerators(true)
16+
.withAsyncBreakCheckInEval(true)
1617
.build();
1718

1819
threadSafeRuntime = facebook::hermes::makeThreadSafeHermesRuntime(config);

test-app/runtime/src/main/cpp/napi/hermes/jsr.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class JSR {
2929
std::unique_ptr<facebook::jsi::ThreadSafeRuntime> threadSafeRuntime;
3030
facebook::hermes::HermesRuntime* rt;
3131
std::recursive_mutex js_mutex;
32+
// Depth of nested JS scopes entered from the host (see NapiScope). Hermes is
33+
// configured with an explicit microtask queue, so promise jobs only run when
34+
// we drain them; we drain once this returns to 0, i.e. when the native call
35+
// stack has fully unwound back out of JS.
36+
int jsEnterState = 0;
3237
void lock() {
3338
threadSafeRuntime->lock();
3439
js_mutex.lock();
@@ -47,6 +52,11 @@ class NapiScope {
4752
: env_(env)
4853
{
4954
js_lock_env(env_);
55+
auto it = JSR::env_to_jsr_cache.find(env_);
56+
jsr_ = it != JSR::env_to_jsr_cache.end() ? it->second : nullptr;
57+
if (jsr_) {
58+
jsr_->jsEnterState++;
59+
}
5060
if (openHandle) {
5161
napi_open_handle_scope(env_, &napiHandleScope_);
5262
} else {
@@ -55,6 +65,18 @@ class NapiScope {
5565
}
5666

5767
~NapiScope() {
68+
// Drain the microtask queue only when the outermost JS scope unwinds so
69+
// that promise continuations (async/await) run — mirroring how a JS
70+
// engine empties its job queue once control returns to the host. Draining
71+
// at a nested depth would run continuations while JS is still on the
72+
// stack. A throwing microtask must never escape a destructor.
73+
if (jsr_ && --jsr_->jsEnterState <= 0) {
74+
jsr_->jsEnterState = 0;
75+
try {
76+
js_execute_pending_jobs(env_);
77+
} catch (...) {
78+
}
79+
}
5880
if (napiHandleScope_) {
5981
napi_close_handle_scope(env_, napiHandleScope_);
6082
}
@@ -64,6 +86,7 @@ class NapiScope {
6486
private:
6587
napi_env env_;
6688
napi_handle_scope napiHandleScope_;
89+
JSR* jsr_ = nullptr;
6790
};
6891

6992
#define JSEnterScope

0 commit comments

Comments
 (0)