@onekeyfe/react-native-background-thread 3.0.17 → 3.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
#include <jni.h>
|
|
2
2
|
#include <jsi/jsi.h>
|
|
3
3
|
#include <android/log.h>
|
|
4
|
+
#include <atomic>
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <condition_variable>
|
|
7
|
+
#include <deque>
|
|
8
|
+
#include <functional>
|
|
4
9
|
#include <memory>
|
|
5
10
|
#include <mutex>
|
|
6
11
|
#include <string>
|
|
12
|
+
#include <thread>
|
|
7
13
|
#include <unordered_map>
|
|
14
|
+
#include <unordered_set>
|
|
8
15
|
|
|
9
16
|
#include "SharedStore.h"
|
|
10
17
|
#include "SharedRPC.h"
|
|
@@ -86,6 +93,355 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
|
|
|
86
93
|
} catch (const std::exception &e) {
|
|
87
94
|
LOGE("Error in nativeExecuteWork: %s", e.what());
|
|
88
95
|
}
|
|
96
|
+
|
|
97
|
+
// CRITICAL: Drain the Hermes microtask queue. React Native 0.74+ configures
|
|
98
|
+
// Hermes with an explicit microtask queue, which must be manually drained
|
|
99
|
+
// after each JS execution. Without this, Promise.then() / async-await
|
|
100
|
+
// continuations (including already-resolved promises) are never executed,
|
|
101
|
+
// causing all awaits to hang forever in the background runtime.
|
|
102
|
+
try {
|
|
103
|
+
rt->drainMicrotasks();
|
|
104
|
+
} catch (const jsi::JSError &e) {
|
|
105
|
+
LOGE("JSError draining microtasks: %s", e.getMessage().c_str());
|
|
106
|
+
} catch (const std::exception &e) {
|
|
107
|
+
LOGE("Error draining microtasks: %s", e.what());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Timer support for background runtime ──────────────────────────────
|
|
112
|
+
// The background Hermes runtime does NOT have working setTimeout/setInterval
|
|
113
|
+
// out of the box (RN's timer module only wires into the main runtime). We
|
|
114
|
+
// install our own JSI-level setTimeout/setInterval/clearTimeout/clearInterval
|
|
115
|
+
// backed by a single C++ worker thread that dispatches callbacks back to the
|
|
116
|
+
// background JS queue via the same executor used by SharedRPC.
|
|
117
|
+
|
|
118
|
+
struct TimerEntry {
|
|
119
|
+
std::shared_ptr<jsi::Function> callback;
|
|
120
|
+
long long fireAtMs; // Absolute time in ms when the timer should fire.
|
|
121
|
+
long long intervalMs; // 0 if one-shot, >0 if setInterval period.
|
|
122
|
+
bool cancelled;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
static std::mutex gTimerMutex;
|
|
126
|
+
static std::condition_variable gTimerCv;
|
|
127
|
+
static std::unordered_map<int64_t, TimerEntry> gTimers;
|
|
128
|
+
static std::atomic<int64_t> gNextTimerId{1};
|
|
129
|
+
static std::atomic<bool> gTimerWorkerStarted{false};
|
|
130
|
+
static std::atomic<bool> gTimerWorkerStop{false};
|
|
131
|
+
static RPCRuntimeExecutor gBgTimerExecutor;
|
|
132
|
+
|
|
133
|
+
static long long nowMs() {
|
|
134
|
+
using namespace std::chrono;
|
|
135
|
+
return duration_cast<milliseconds>(
|
|
136
|
+
steady_clock::now().time_since_epoch())
|
|
137
|
+
.count();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Called on the bg JS thread. Executes the callback only; the worker has
|
|
141
|
+
// already erased (one-shot) or rescheduled (interval) the timer under lock.
|
|
142
|
+
static void fireTimerOnJsThread(
|
|
143
|
+
int64_t timerId,
|
|
144
|
+
std::shared_ptr<jsi::Function> cb,
|
|
145
|
+
jsi::Runtime &rt) {
|
|
146
|
+
if (!cb) return;
|
|
147
|
+
try {
|
|
148
|
+
cb->call(rt);
|
|
149
|
+
} catch (const jsi::JSError &e) {
|
|
150
|
+
LOGE("Timer %lld callback JSError: %s", (long long)timerId,
|
|
151
|
+
e.getMessage().c_str());
|
|
152
|
+
} catch (const std::exception &e) {
|
|
153
|
+
LOGE("Timer %lld callback error: %s", (long long)timerId, e.what());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static void timerWorkerLoop() {
|
|
158
|
+
while (!gTimerWorkerStop.load()) {
|
|
159
|
+
// Snapshot of timers that should be dispatched this iteration and
|
|
160
|
+
// their callbacks. Captured under the lock; callbacks are invoked on
|
|
161
|
+
// the JS thread (not here).
|
|
162
|
+
std::vector<std::pair<int64_t, std::shared_ptr<jsi::Function>>> toFire;
|
|
163
|
+
{
|
|
164
|
+
std::unique_lock<std::mutex> lock(gTimerMutex);
|
|
165
|
+
if (gTimers.empty()) {
|
|
166
|
+
gTimerCv.wait(lock, [] {
|
|
167
|
+
return gTimerWorkerStop.load() || !gTimers.empty();
|
|
168
|
+
});
|
|
169
|
+
if (gTimerWorkerStop.load()) return;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Find the earliest fireAt among non-cancelled timers.
|
|
174
|
+
long long earliest = LLONG_MAX;
|
|
175
|
+
for (auto &kv : gTimers) {
|
|
176
|
+
if (!kv.second.cancelled && kv.second.fireAtMs < earliest) {
|
|
177
|
+
earliest = kv.second.fireAtMs;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
long long now = nowMs();
|
|
181
|
+
if (earliest == LLONG_MAX) {
|
|
182
|
+
// Only cancelled timers remain; clean them up.
|
|
183
|
+
for (auto it = gTimers.begin(); it != gTimers.end();) {
|
|
184
|
+
if (it->second.cancelled) it = gTimers.erase(it);
|
|
185
|
+
else ++it;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (earliest > now) {
|
|
190
|
+
gTimerCv.wait_for(
|
|
191
|
+
lock, std::chrono::milliseconds(earliest - now));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Collect ready timers AND either erase (one-shot) or reschedule
|
|
196
|
+
// (interval) them RIGHT HERE under the lock. This is critical:
|
|
197
|
+
// if we wait to erase in fireTimerOnJsThread, the next worker
|
|
198
|
+
// iteration would immediately find the same timers still
|
|
199
|
+
// in-queue and re-dispatch them, causing an infinite flood of
|
|
200
|
+
// `scheduleOnJSThread` calls.
|
|
201
|
+
for (auto it = gTimers.begin(); it != gTimers.end();) {
|
|
202
|
+
if (it->second.cancelled) {
|
|
203
|
+
it = gTimers.erase(it);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (it->second.fireAtMs <= now) {
|
|
207
|
+
toFire.emplace_back(it->first, it->second.callback);
|
|
208
|
+
if (it->second.intervalMs > 0) {
|
|
209
|
+
// Reschedule interval. Use `now + intervalMs` rather
|
|
210
|
+
// than `fireAtMs + intervalMs` so a slow fire path
|
|
211
|
+
// cannot produce an infinite backlog.
|
|
212
|
+
it->second.fireAtMs = now + it->second.intervalMs;
|
|
213
|
+
++it;
|
|
214
|
+
} else {
|
|
215
|
+
it = gTimers.erase(it);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
++it;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
RPCRuntimeExecutor executor = gBgTimerExecutor;
|
|
224
|
+
if (!executor) {
|
|
225
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
for (auto &pair : toFire) {
|
|
229
|
+
int64_t id = pair.first;
|
|
230
|
+
std::shared_ptr<jsi::Function> cb = pair.second;
|
|
231
|
+
executor([id, cb](jsi::Runtime &rt) {
|
|
232
|
+
fireTimerOnJsThread(id, cb, rt);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static void ensureTimerWorkerStarted() {
|
|
239
|
+
bool expected = false;
|
|
240
|
+
if (gTimerWorkerStarted.compare_exchange_strong(expected, true)) {
|
|
241
|
+
std::thread(timerWorkerLoop).detach();
|
|
242
|
+
LOGI("Timer worker thread started");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static int64_t scheduleTimer(
|
|
247
|
+
std::shared_ptr<jsi::Function> cb,
|
|
248
|
+
double ms,
|
|
249
|
+
bool isInterval) {
|
|
250
|
+
int64_t id = gNextTimerId.fetch_add(1);
|
|
251
|
+
long long intervalMs = isInterval ? static_cast<long long>(ms) : 0;
|
|
252
|
+
long long delay = static_cast<long long>(ms);
|
|
253
|
+
if (delay < 0) delay = 0;
|
|
254
|
+
{
|
|
255
|
+
std::lock_guard<std::mutex> lock(gTimerMutex);
|
|
256
|
+
gTimers[id] = TimerEntry{
|
|
257
|
+
std::move(cb),
|
|
258
|
+
nowMs() + delay,
|
|
259
|
+
intervalMs,
|
|
260
|
+
false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
gTimerCv.notify_all();
|
|
264
|
+
ensureTimerWorkerStarted();
|
|
265
|
+
return id;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static void cancelTimer(int64_t id) {
|
|
269
|
+
{
|
|
270
|
+
std::lock_guard<std::mutex> lock(gTimerMutex);
|
|
271
|
+
auto it = gTimers.find(id);
|
|
272
|
+
if (it != gTimers.end()) {
|
|
273
|
+
it->second.cancelled = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
gTimerCv.notify_all();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static void installTimersOnRuntime(jsi::Runtime &rt) {
|
|
280
|
+
auto makeSetter = [](bool isInterval) {
|
|
281
|
+
return [isInterval](
|
|
282
|
+
jsi::Runtime &rt,
|
|
283
|
+
const jsi::Value &,
|
|
284
|
+
const jsi::Value *args,
|
|
285
|
+
size_t count) -> jsi::Value {
|
|
286
|
+
if (count < 1 || !args[0].isObject() ||
|
|
287
|
+
!args[0].getObject(rt).isFunction(rt)) {
|
|
288
|
+
return jsi::Value::undefined();
|
|
289
|
+
}
|
|
290
|
+
auto cb = std::make_shared<jsi::Function>(
|
|
291
|
+
args[0].getObject(rt).getFunction(rt));
|
|
292
|
+
double ms = 0;
|
|
293
|
+
if (count >= 2 && args[1].isNumber()) {
|
|
294
|
+
ms = args[1].asNumber();
|
|
295
|
+
}
|
|
296
|
+
int64_t id = scheduleTimer(std::move(cb), ms, isInterval);
|
|
297
|
+
return jsi::Value(static_cast<double>(id));
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
auto makeCanceller = []() {
|
|
301
|
+
return [](jsi::Runtime &rt,
|
|
302
|
+
const jsi::Value &,
|
|
303
|
+
const jsi::Value *args,
|
|
304
|
+
size_t count) -> jsi::Value {
|
|
305
|
+
if (count < 1 || !args[0].isNumber()) {
|
|
306
|
+
return jsi::Value::undefined();
|
|
307
|
+
}
|
|
308
|
+
int64_t id = static_cast<int64_t>(args[0].asNumber());
|
|
309
|
+
cancelTimer(id);
|
|
310
|
+
return jsi::Value::undefined();
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// requestAnimationFrame(cb): fires after ~16ms (60fps) with high-resolution
|
|
315
|
+
// timestamp arg, matching the DOM contract. Background runtime has no
|
|
316
|
+
// rendering concept, so we just approximate via setTimeout(16ms).
|
|
317
|
+
auto rafFn = [](jsi::Runtime &rt,
|
|
318
|
+
const jsi::Value &,
|
|
319
|
+
const jsi::Value *args,
|
|
320
|
+
size_t count) -> jsi::Value {
|
|
321
|
+
if (count < 1 || !args[0].isObject() ||
|
|
322
|
+
!args[0].getObject(rt).isFunction(rt)) {
|
|
323
|
+
return jsi::Value::undefined();
|
|
324
|
+
}
|
|
325
|
+
// Wrap callback so it receives a DOMHighResTimeStamp-like arg.
|
|
326
|
+
auto userCb = std::make_shared<jsi::Function>(
|
|
327
|
+
args[0].getObject(rt).getFunction(rt));
|
|
328
|
+
auto wrapper = jsi::Function::createFromHostFunction(
|
|
329
|
+
rt,
|
|
330
|
+
jsi::PropNameID::forAscii(rt, "rafWrapper"),
|
|
331
|
+
0,
|
|
332
|
+
[userCb](jsi::Runtime &rt2,
|
|
333
|
+
const jsi::Value &,
|
|
334
|
+
const jsi::Value *,
|
|
335
|
+
size_t) -> jsi::Value {
|
|
336
|
+
try {
|
|
337
|
+
userCb->call(rt2, jsi::Value(static_cast<double>(nowMs())));
|
|
338
|
+
} catch (const jsi::JSError &e) {
|
|
339
|
+
LOGE("rAF callback JSError: %s", e.getMessage().c_str());
|
|
340
|
+
} catch (const std::exception &e) {
|
|
341
|
+
LOGE("rAF callback error: %s", e.what());
|
|
342
|
+
}
|
|
343
|
+
return jsi::Value::undefined();
|
|
344
|
+
});
|
|
345
|
+
auto wrappedCb = std::make_shared<jsi::Function>(std::move(wrapper));
|
|
346
|
+
int64_t id = scheduleTimer(std::move(wrappedCb), 16.0, false);
|
|
347
|
+
return jsi::Value(static_cast<double>(id));
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// requestIdleCallback(cb, {timeout?}): fires "soon" with an IdleDeadline-ish
|
|
351
|
+
// object. Background runtime has no render frames to be idle between, so
|
|
352
|
+
// we approximate via setTimeout(1ms) and provide a deadline stub whose
|
|
353
|
+
// timeRemaining() always returns 50 (reasonable budget).
|
|
354
|
+
auto ricFn = [](jsi::Runtime &rt,
|
|
355
|
+
const jsi::Value &,
|
|
356
|
+
const jsi::Value *args,
|
|
357
|
+
size_t count) -> jsi::Value {
|
|
358
|
+
if (count < 1 || !args[0].isObject() ||
|
|
359
|
+
!args[0].getObject(rt).isFunction(rt)) {
|
|
360
|
+
return jsi::Value::undefined();
|
|
361
|
+
}
|
|
362
|
+
auto userCb = std::make_shared<jsi::Function>(
|
|
363
|
+
args[0].getObject(rt).getFunction(rt));
|
|
364
|
+
auto wrapper = jsi::Function::createFromHostFunction(
|
|
365
|
+
rt,
|
|
366
|
+
jsi::PropNameID::forAscii(rt, "ricWrapper"),
|
|
367
|
+
0,
|
|
368
|
+
[userCb](jsi::Runtime &rt2,
|
|
369
|
+
const jsi::Value &,
|
|
370
|
+
const jsi::Value *,
|
|
371
|
+
size_t) -> jsi::Value {
|
|
372
|
+
try {
|
|
373
|
+
// Build a minimal IdleDeadline: { didTimeout: false,
|
|
374
|
+
// timeRemaining: () => 50 }.
|
|
375
|
+
jsi::Object deadline(rt2);
|
|
376
|
+
deadline.setProperty(rt2, "didTimeout", jsi::Value(false));
|
|
377
|
+
deadline.setProperty(
|
|
378
|
+
rt2,
|
|
379
|
+
"timeRemaining",
|
|
380
|
+
jsi::Function::createFromHostFunction(
|
|
381
|
+
rt2,
|
|
382
|
+
jsi::PropNameID::forAscii(rt2, "timeRemaining"),
|
|
383
|
+
0,
|
|
384
|
+
[](jsi::Runtime &,
|
|
385
|
+
const jsi::Value &,
|
|
386
|
+
const jsi::Value *,
|
|
387
|
+
size_t) -> jsi::Value {
|
|
388
|
+
return jsi::Value(50.0);
|
|
389
|
+
}));
|
|
390
|
+
userCb->call(rt2, jsi::Value(rt2, std::move(deadline)));
|
|
391
|
+
} catch (const jsi::JSError &e) {
|
|
392
|
+
LOGE("rIC callback JSError: %s", e.getMessage().c_str());
|
|
393
|
+
} catch (const std::exception &e) {
|
|
394
|
+
LOGE("rIC callback error: %s", e.what());
|
|
395
|
+
}
|
|
396
|
+
return jsi::Value::undefined();
|
|
397
|
+
});
|
|
398
|
+
auto wrappedCb = std::make_shared<jsi::Function>(std::move(wrapper));
|
|
399
|
+
int64_t id = scheduleTimer(std::move(wrappedCb), 1.0, false);
|
|
400
|
+
return jsi::Value(static_cast<double>(id));
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
auto global = rt.global();
|
|
404
|
+
global.setProperty(
|
|
405
|
+
rt, "setTimeout",
|
|
406
|
+
jsi::Function::createFromHostFunction(
|
|
407
|
+
rt, jsi::PropNameID::forAscii(rt, "setTimeout"), 2,
|
|
408
|
+
makeSetter(false)));
|
|
409
|
+
global.setProperty(
|
|
410
|
+
rt, "setInterval",
|
|
411
|
+
jsi::Function::createFromHostFunction(
|
|
412
|
+
rt, jsi::PropNameID::forAscii(rt, "setInterval"), 2,
|
|
413
|
+
makeSetter(true)));
|
|
414
|
+
global.setProperty(
|
|
415
|
+
rt, "clearTimeout",
|
|
416
|
+
jsi::Function::createFromHostFunction(
|
|
417
|
+
rt, jsi::PropNameID::forAscii(rt, "clearTimeout"), 1,
|
|
418
|
+
makeCanceller()));
|
|
419
|
+
global.setProperty(
|
|
420
|
+
rt, "clearInterval",
|
|
421
|
+
jsi::Function::createFromHostFunction(
|
|
422
|
+
rt, jsi::PropNameID::forAscii(rt, "clearInterval"), 1,
|
|
423
|
+
makeCanceller()));
|
|
424
|
+
global.setProperty(
|
|
425
|
+
rt, "requestAnimationFrame",
|
|
426
|
+
jsi::Function::createFromHostFunction(
|
|
427
|
+
rt, jsi::PropNameID::forAscii(rt, "requestAnimationFrame"), 1,
|
|
428
|
+
rafFn));
|
|
429
|
+
global.setProperty(
|
|
430
|
+
rt, "cancelAnimationFrame",
|
|
431
|
+
jsi::Function::createFromHostFunction(
|
|
432
|
+
rt, jsi::PropNameID::forAscii(rt, "cancelAnimationFrame"), 1,
|
|
433
|
+
makeCanceller()));
|
|
434
|
+
global.setProperty(
|
|
435
|
+
rt, "requestIdleCallback",
|
|
436
|
+
jsi::Function::createFromHostFunction(
|
|
437
|
+
rt, jsi::PropNameID::forAscii(rt, "requestIdleCallback"), 1,
|
|
438
|
+
ricFn));
|
|
439
|
+
global.setProperty(
|
|
440
|
+
rt, "cancelIdleCallback",
|
|
441
|
+
jsi::Function::createFromHostFunction(
|
|
442
|
+
rt, jsi::PropNameID::forAscii(rt, "cancelIdleCallback"), 1,
|
|
443
|
+
makeCanceller()));
|
|
444
|
+
LOGI("Timer + rAF + rIC polyfills installed on bg runtime");
|
|
89
445
|
}
|
|
90
446
|
|
|
91
447
|
// ── nativeInstallSharedBridge ───────────────────────────────────────────
|
|
@@ -145,9 +501,20 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
|
|
|
145
501
|
};
|
|
146
502
|
|
|
147
503
|
std::string runtimeId = isMain ? "main" : "background";
|
|
504
|
+
// Save the bg executor so our custom timer worker can dispatch callbacks
|
|
505
|
+
// back to the bg JS queue. We must do this BEFORE moving `executor` into
|
|
506
|
+
// SharedRPC::install (which will std::move it out).
|
|
507
|
+
if (!capturedIsMain) {
|
|
508
|
+
gBgTimerExecutor = executor;
|
|
509
|
+
}
|
|
148
510
|
SharedRPC::install(*rt, std::move(executor), runtimeId);
|
|
149
511
|
LOGI("SharedStore and SharedRPC installed (isMain=%d)", static_cast<int>(isMain));
|
|
150
512
|
if (!capturedIsMain) {
|
|
513
|
+
// Install setTimeout/setInterval/clearTimeout/clearInterval on the
|
|
514
|
+
// background runtime. React Native's built-in timer module only wires
|
|
515
|
+
// into the main runtime, so without this, any `await wait(ms)` or
|
|
516
|
+
// setTimeout callback in the background thread would never fire.
|
|
517
|
+
installTimersOnRuntime(*rt);
|
|
151
518
|
invokeOptionalGlobalFunction(*rt, "__setupBackgroundRPCHandler");
|
|
152
519
|
}
|
|
153
520
|
}
|