@onekeyfe/react-native-background-thread 3.0.61 → 3.0.63
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.
- package/android/src/main/cpp/cpp-adapter.cpp +168 -394
- package/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt +48 -150
- package/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt +3 -11
- package/ios/BackgroundThread.mm +1 -48
- package/ios/BackgroundThreadManager.h +0 -43
- package/ios/BackgroundThreadManager.mm +10 -219
- package/package.json +1 -1
- package/src/SharedRPC.ts +1 -1
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
#include <atomic>
|
|
5
5
|
#include <chrono>
|
|
6
6
|
#include <condition_variable>
|
|
7
|
-
#include <cstdio>
|
|
8
7
|
#include <deque>
|
|
9
8
|
#include <functional>
|
|
10
9
|
#include <memory>
|
|
@@ -71,6 +70,166 @@ static std::mutex gWorkMutex;
|
|
|
71
70
|
static std::unordered_map<int64_t, std::function<void(jsi::Runtime &)>> gPendingWork;
|
|
72
71
|
static int64_t gNextWorkId = 0;
|
|
73
72
|
|
|
73
|
+
using JavaObjectRef = std::shared_ptr<_jobject>;
|
|
74
|
+
|
|
75
|
+
static constexpr size_t kRuntimeDrainBatchSize = 64;
|
|
76
|
+
static constexpr size_t kRuntimeQueueWarnThreshold = 128;
|
|
77
|
+
static constexpr size_t kRuntimeQueueWarnInterval = 128;
|
|
78
|
+
|
|
79
|
+
struct RuntimeWorkQueue {
|
|
80
|
+
std::deque<std::function<void(jsi::Runtime &)>> items;
|
|
81
|
+
bool drainScheduled = false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
static RuntimeWorkQueue gMainRuntimeWorkQueue;
|
|
85
|
+
static RuntimeWorkQueue gBgRuntimeWorkQueue;
|
|
86
|
+
|
|
87
|
+
static RuntimeWorkQueue &getRuntimeWorkQueue(bool isMain) {
|
|
88
|
+
return isMain ? gMainRuntimeWorkQueue : gBgRuntimeWorkQueue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static bool callScheduleOnJSThread(const JavaObjectRef &ref, bool isMain, int64_t workId) {
|
|
92
|
+
JNIEnv *env = getJNIEnv();
|
|
93
|
+
if (!env || !ref) {
|
|
94
|
+
LOGE("executor: env=%p, ref=%p — aborting", env, ref.get());
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
jclass cls = env->GetObjectClass(ref.get());
|
|
99
|
+
if (!cls) {
|
|
100
|
+
LOGE("executor: GetObjectClass failed");
|
|
101
|
+
if (env->ExceptionCheck()) {
|
|
102
|
+
env->ExceptionDescribe();
|
|
103
|
+
env->ExceptionClear();
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)Z");
|
|
109
|
+
if (!mid) {
|
|
110
|
+
LOGE("executor: scheduleOnJSThread method not found!");
|
|
111
|
+
if (env->ExceptionCheck()) {
|
|
112
|
+
env->ExceptionDescribe();
|
|
113
|
+
env->ExceptionClear();
|
|
114
|
+
}
|
|
115
|
+
env->DeleteLocalRef(cls);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", isMain, (long)workId);
|
|
120
|
+
jboolean scheduled = env->CallBooleanMethod(
|
|
121
|
+
ref.get(),
|
|
122
|
+
mid,
|
|
123
|
+
static_cast<jboolean>(isMain),
|
|
124
|
+
static_cast<jlong>(workId));
|
|
125
|
+
if (env->ExceptionCheck()) {
|
|
126
|
+
LOGE("executor: JNI exception after scheduleOnJSThread");
|
|
127
|
+
env->ExceptionDescribe();
|
|
128
|
+
env->ExceptionClear();
|
|
129
|
+
env->DeleteLocalRef(cls);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
env->DeleteLocalRef(cls);
|
|
133
|
+
return scheduled == JNI_TRUE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain);
|
|
137
|
+
|
|
138
|
+
static void drainRuntimeWorkQueue(jsi::Runtime &rt, JavaObjectRef ref, bool isMain) {
|
|
139
|
+
size_t drained = 0;
|
|
140
|
+
|
|
141
|
+
while (drained < kRuntimeDrainBatchSize) {
|
|
142
|
+
std::function<void(jsi::Runtime &)> work;
|
|
143
|
+
{
|
|
144
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
145
|
+
auto &queue = getRuntimeWorkQueue(isMain);
|
|
146
|
+
if (queue.items.empty()) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
work = std::move(queue.items.front());
|
|
150
|
+
queue.items.pop_front();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
work(rt);
|
|
155
|
+
} catch (const jsi::JSError &e) {
|
|
156
|
+
LOGE("JSError in runtime drain work: %s", e.getMessage().c_str());
|
|
157
|
+
} catch (const std::exception &e) {
|
|
158
|
+
LOGE("Error in runtime drain work: %s", e.what());
|
|
159
|
+
} catch (...) {
|
|
160
|
+
LOGE("Unknown error in runtime drain work");
|
|
161
|
+
}
|
|
162
|
+
drained += 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
bool shouldReschedule = false;
|
|
166
|
+
size_t remaining = 0;
|
|
167
|
+
{
|
|
168
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
169
|
+
auto &queue = getRuntimeWorkQueue(isMain);
|
|
170
|
+
remaining = queue.items.size();
|
|
171
|
+
if (remaining == 0) {
|
|
172
|
+
queue.drainScheduled = false;
|
|
173
|
+
} else {
|
|
174
|
+
shouldReschedule = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (drained > 1 || remaining > 0) {
|
|
179
|
+
LOGI("executor: drained runtime queue isMain=%d, drained=%zu, remaining=%zu",
|
|
180
|
+
isMain, drained, remaining);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (shouldReschedule) {
|
|
184
|
+
scheduleRuntimeDrain(ref, isMain);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain) {
|
|
189
|
+
int64_t workId;
|
|
190
|
+
size_t queued = 0;
|
|
191
|
+
{
|
|
192
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
193
|
+
workId = gNextWorkId++;
|
|
194
|
+
queued = getRuntimeWorkQueue(isMain).items.size();
|
|
195
|
+
gPendingWork[workId] = [ref, isMain](jsi::Runtime &rt) {
|
|
196
|
+
drainRuntimeWorkQueue(rt, ref, isMain);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
bool scheduled = callScheduleOnJSThread(ref, isMain, workId);
|
|
201
|
+
if (!scheduled) {
|
|
202
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
203
|
+
gPendingWork.erase(workId);
|
|
204
|
+
getRuntimeWorkQueue(isMain).drainScheduled = false;
|
|
205
|
+
LOGE("executor: failed to schedule runtime drain isMain=%d, workId=%ld, queued=%zu",
|
|
206
|
+
isMain, (long)workId, queued);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static void enqueueRuntimeWork(JavaObjectRef ref, bool isMain, std::function<void(jsi::Runtime &)> work) {
|
|
211
|
+
bool shouldSchedule = false;
|
|
212
|
+
size_t queued = 0;
|
|
213
|
+
{
|
|
214
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
215
|
+
auto &queue = getRuntimeWorkQueue(isMain);
|
|
216
|
+
queue.items.push_back(std::move(work));
|
|
217
|
+
queued = queue.items.size();
|
|
218
|
+
if (!queue.drainScheduled) {
|
|
219
|
+
queue.drainScheduled = true;
|
|
220
|
+
shouldSchedule = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (queued >= kRuntimeQueueWarnThreshold && queued % kRuntimeQueueWarnInterval == 0) {
|
|
225
|
+
LOGE("executor: runtime queue backlog isMain=%d, queued=%zu", isMain, queued);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (shouldSchedule) {
|
|
229
|
+
scheduleRuntimeDrain(ref, isMain);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
74
233
|
// Called from Kotlin after runOnJSQueueThread dispatches to the correct thread.
|
|
75
234
|
extern "C" JNIEXPORT void JNICALL
|
|
76
235
|
Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
|
|
@@ -448,323 +607,6 @@ static void installTimersOnRuntime(jsi::Runtime &rt) {
|
|
|
448
607
|
LOGI("Timer + rAF + rIC polyfills installed on bg runtime");
|
|
449
608
|
}
|
|
450
609
|
|
|
451
|
-
// ── Background segment eval (fix A: eval-then-resolve on the BG runtime) ──
|
|
452
|
-
//
|
|
453
|
-
// WHY THIS EXISTS — the "Requiring unknown module" race on the BACKGROUND
|
|
454
|
-
// runtime:
|
|
455
|
-
// The Kotlin BackgroundThreadManager.registerSegmentInBackground previously
|
|
456
|
-
// called `ReactContext.registerSegment(...)`, which (bridgeless) routes through
|
|
457
|
-
// ReactHostImpl.registerSegment → ReactInstance.registerSegment → C++
|
|
458
|
-
// ReactInstance::registerSegment, and that only `scheduleWork`s the
|
|
459
|
-
// evaluateJavaScript onto the RuntimeScheduler before returning. The Kotlin
|
|
460
|
-
// completion callback then fired immediately, so the JS promise resolved BEFORE
|
|
461
|
-
// the segment's `__d(...)` module definitions ran. Metro's
|
|
462
|
-
// `import().then(() => __r(moduleId))` could then run `__r` before the module
|
|
463
|
-
// table was populated → a fatal, uncatchable "Requiring unknown module". Locale
|
|
464
|
-
// segments load through THIS bg path, so a language switch could still crash.
|
|
465
|
-
//
|
|
466
|
-
// THE FIX (mirrors the MAIN-runtime SplitBundleLoaderJSI fix and the iOS
|
|
467
|
-
// callFunctionOnBufferedRuntimeExecutor: fix):
|
|
468
|
-
// We evaluate the segment OURSELVES on the BACKGROUND JS thread and signal
|
|
469
|
-
// completion in the SAME block, strictly AFTER eval. The accessor we use is the
|
|
470
|
-
// background runtime's own RuntimeExecutor (`gBgTimerExecutor`), captured in
|
|
471
|
-
// nativeInstallSharedBridge for the bg runtime (isMain=false). It dispatches via
|
|
472
|
-
// scheduleOnJSThread(isMain=false, ...) → nativeExecuteWork, which runs the work
|
|
473
|
-
// on the bg JS queue thread and then drainMicrotasks() — so this targets the
|
|
474
|
-
// BACKGROUND runtime, NOT the main one. This is the correct bg analogue of the
|
|
475
|
-
// main path's CallInvoker (the bg runtime is created by ReactHostImpl and the bg
|
|
476
|
-
// ReactContext does not surface a usable jsCallInvokerHolder the way the main
|
|
477
|
-
// one does, but its RuntimeExecutor already exists and routes to the bg JS
|
|
478
|
-
// thread).
|
|
479
|
-
//
|
|
480
|
-
// Off-thread read (fix F): the segment file is read on the CALLING (native
|
|
481
|
-
// module) thread before dispatch; only evaluateJavaScript + completion run on
|
|
482
|
-
// the bg JS thread.
|
|
483
|
-
|
|
484
|
-
// Java callback contract — implemented in Kotlin as
|
|
485
|
-
// BackgroundThreadManager.SegmentEvalCallback.onComplete(error). Empty/null
|
|
486
|
-
// error string → success. A message prefixed with "NO_RUNTIME:" → bg runtime
|
|
487
|
-
// not ready (retryable); "IO_ERROR:" → file read failure (fatal); otherwise →
|
|
488
|
-
// eval throw (fatal). Resolved exactly once on the Kotlin side via its watchdog
|
|
489
|
-
// guard.
|
|
490
|
-
static void invokeBgSegmentCallback(jobject globalCallback, const std::string &error) {
|
|
491
|
-
JNIEnv *env = getJNIEnv();
|
|
492
|
-
if (!env || !globalCallback) {
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
jclass cls = env->GetObjectClass(globalCallback);
|
|
496
|
-
jmethodID mid =
|
|
497
|
-
env->GetMethodID(cls, "onComplete", "(Ljava/lang/String;)V");
|
|
498
|
-
if (mid) {
|
|
499
|
-
jstring jerr = error.empty() ? nullptr : env->NewStringUTF(error.c_str());
|
|
500
|
-
env->CallVoidMethod(globalCallback, mid, jerr);
|
|
501
|
-
if (env->ExceptionCheck()) {
|
|
502
|
-
LOGE("invokeBgSegmentCallback: JNI exception after onComplete");
|
|
503
|
-
env->ExceptionDescribe();
|
|
504
|
-
env->ExceptionClear();
|
|
505
|
-
}
|
|
506
|
-
if (jerr) {
|
|
507
|
-
env->DeleteLocalRef(jerr);
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
LOGE("invokeBgSegmentCallback: onComplete method not found!");
|
|
511
|
-
if (env->ExceptionCheck()) {
|
|
512
|
-
env->ExceptionDescribe();
|
|
513
|
-
env->ExceptionClear();
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
env->DeleteLocalRef(cls);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ── Pending bg-eval callback registry (fix: bounded global-ref lifetime) ──
|
|
520
|
-
//
|
|
521
|
-
// The bg-eval work lambda captures a JNI global ref to the SegmentEvalCallback
|
|
522
|
-
// and is enqueued onto gPendingWork for the bg JS thread. If that work is
|
|
523
|
-
// enqueued but NEVER runs, the captured global ref would leak and the JS promise
|
|
524
|
-
// would settle only via the Kotlin 30s watchdog. The drop paths that release it:
|
|
525
|
-
// - Java schedule fails (JNI exception / missing scheduleOnJSThread): the C++
|
|
526
|
-
// executor erases gPendingWork[workId] and drains this eval.
|
|
527
|
-
// - bg context==null / ptr==0 in scheduleOnJSThread: Kotlin calls
|
|
528
|
-
// nativeDropScheduledWork(workId) — which erases the work AND drains —
|
|
529
|
-
// BEFORE it would call nativeExecuteWork. (nativeExecuteWork's own
|
|
530
|
-
// rt==nullptr guard merely returns and is NOT a drain path; it is never
|
|
531
|
-
// reached for a dead ptr because Kotlin intercepts that case first.)
|
|
532
|
-
// - nativeDestroy: intentionally leaks the work lambdas (their ~jsi::Function
|
|
533
|
-
// can't run on a torn-down runtime) but drains this registry to settle them.
|
|
534
|
-
// To make this strictly bounded, every in-flight bg eval registers here. Whoever
|
|
535
|
-
// settles it first (the work lambda after eval, OR a drain on a drop path) claims
|
|
536
|
-
// it via `settled`, invokes the Java callback exactly once, and deletes the
|
|
537
|
-
// global ref. This guarantees no leak and no double-invoke.
|
|
538
|
-
struct PendingBgEval {
|
|
539
|
-
jobject globalCallback; // owned: deleted by whoever settles
|
|
540
|
-
std::shared_ptr<std::atomic<bool>> settled; // exactly-once claim
|
|
541
|
-
};
|
|
542
|
-
static std::mutex gBgEvalMutex;
|
|
543
|
-
static std::unordered_map<int64_t, PendingBgEval> gPendingBgEvals;
|
|
544
|
-
static int64_t gNextBgEvalId = 0;
|
|
545
|
-
|
|
546
|
-
// Settle a pending bg eval exactly once: invoke the Java callback with `error`
|
|
547
|
-
// (empty => success) and release the global ref. The shared `settled` flag is
|
|
548
|
-
// the single source of truth for the one-shot — the registry entry may already
|
|
549
|
-
// be gone (claimed/erased by the other party), so this is self-contained.
|
|
550
|
-
static void settleBgEval(jobject globalCallback,
|
|
551
|
-
const std::shared_ptr<std::atomic<bool>> &settled,
|
|
552
|
-
const std::string &error) {
|
|
553
|
-
if (!settled) {
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
bool expected = false;
|
|
557
|
-
if (!settled->compare_exchange_strong(expected, true)) {
|
|
558
|
-
// Already settled by the other party (lambda vs drain). Do nothing —
|
|
559
|
-
// the winner already invoked + deleted the global ref.
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
invokeBgSegmentCallback(globalCallback, error);
|
|
563
|
-
JNIEnv *env = getJNIEnv();
|
|
564
|
-
if (env && globalCallback) {
|
|
565
|
-
env->DeleteGlobalRef(globalCallback);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Settle ALL currently-registered bg evals with a retryable NO_RUNTIME-class
|
|
570
|
-
// failure and clear the registry. Used when the bg runtime is going away (or is
|
|
571
|
-
// unreachable) and any enqueued-but-unrun eval would otherwise leak its global
|
|
572
|
-
// ref and hang the JS promise until the Kotlin watchdog. Each settle is
|
|
573
|
-
// exactly-once (the work lambda may race us, but the shared flag arbitrates),
|
|
574
|
-
// so this never double-invokes.
|
|
575
|
-
static void drainPendingBgEvals(const std::string &reason) {
|
|
576
|
-
// Move the entries OUT under the lock and clear the registry, then settle
|
|
577
|
-
// OUTSIDE the lock. settleBgEval performs a Java upcall (onComplete via
|
|
578
|
-
// CallVoidMethod), so holding gBgEvalMutex across it would hold a native
|
|
579
|
-
// lock across arbitrary JS — a re-entrancy / deadlock hazard if a callback
|
|
580
|
-
// ever synchronously re-enters a gBgEvalMutex-taking path. PendingBgEval is
|
|
581
|
-
// copyable (jobject handle + shared_ptr); copies share the same global ref
|
|
582
|
-
// and `settled` flag, and the shared flag still arbitrates exactly-once
|
|
583
|
-
// against a racing work lambda, so the global ref is released exactly once.
|
|
584
|
-
std::vector<PendingBgEval> drained;
|
|
585
|
-
{
|
|
586
|
-
std::lock_guard<std::mutex> lock(gBgEvalMutex);
|
|
587
|
-
if (gPendingBgEvals.empty()) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
LOGE("[SplitBundle] draining %zu pending bg eval(s): %s",
|
|
591
|
-
gPendingBgEvals.size(), reason.c_str());
|
|
592
|
-
drained.reserve(gPendingBgEvals.size());
|
|
593
|
-
for (auto &entry : gPendingBgEvals) {
|
|
594
|
-
drained.push_back(entry.second);
|
|
595
|
-
}
|
|
596
|
-
gPendingBgEvals.clear();
|
|
597
|
-
}
|
|
598
|
-
for (auto &entry : drained) {
|
|
599
|
-
settleBgEval(entry.globalCallback, entry.settled, "NO_RUNTIME:" + reason);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Reads the whole file at `path` into `out`. Returns false on failure.
|
|
604
|
-
static bool readBgFileToString(const std::string &path, std::string &out) {
|
|
605
|
-
FILE *f = std::fopen(path.c_str(), "rb");
|
|
606
|
-
if (f == nullptr) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
if (std::fseek(f, 0, SEEK_END) != 0) {
|
|
610
|
-
std::fclose(f);
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
long size = std::ftell(f);
|
|
614
|
-
if (size < 0) {
|
|
615
|
-
std::fclose(f);
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
if (std::fseek(f, 0, SEEK_SET) != 0) {
|
|
619
|
-
std::fclose(f);
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
out.resize(static_cast<size_t>(size));
|
|
623
|
-
size_t readBytes =
|
|
624
|
-
(size == 0) ? 0 : std::fread(&out[0], 1, static_cast<size_t>(size), f);
|
|
625
|
-
std::fclose(f);
|
|
626
|
-
return readBytes == static_cast<size_t>(size);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// nativeEvaluateSegmentInBackground: schedule eval of the segment at `path`
|
|
630
|
-
// onto the BACKGROUND JS thread via the bg RuntimeExecutor and invoke `callback`
|
|
631
|
-
// from INSIDE that same block, strictly AFTER eval. Returns immediately; the
|
|
632
|
-
// callback fires later on the bg JS thread (or synchronously here on a fail-fast
|
|
633
|
-
// path such as bg runtime not ready / file read failure).
|
|
634
|
-
extern "C" JNIEXPORT void JNICALL
|
|
635
|
-
Java_com_backgroundthread_BackgroundThreadManager_nativeEvaluateSegmentInBackground(
|
|
636
|
-
JNIEnv *env, jobject /* thiz */, jstring segmentPath, jstring sourceURL,
|
|
637
|
-
jobject callback) {
|
|
638
|
-
|
|
639
|
-
// Global-ref the callback: it is invoked later on a different thread.
|
|
640
|
-
jobject globalCallback = env->NewGlobalRef(callback);
|
|
641
|
-
// Shared one-shot claim flag for this eval — used by BOTH the work lambda
|
|
642
|
-
// (after eval) and any drop-path drain, so exactly one of them invokes the
|
|
643
|
-
// callback + deletes the global ref.
|
|
644
|
-
auto settled = std::make_shared<std::atomic<bool>>(false);
|
|
645
|
-
|
|
646
|
-
const char *pathChars = segmentPath ? env->GetStringUTFChars(segmentPath, nullptr) : nullptr;
|
|
647
|
-
std::string path = pathChars ? std::string(pathChars) : std::string();
|
|
648
|
-
if (pathChars) env->ReleaseStringUTFChars(segmentPath, pathChars);
|
|
649
|
-
|
|
650
|
-
const char *urlChars = sourceURL ? env->GetStringUTFChars(sourceURL, nullptr) : nullptr;
|
|
651
|
-
std::string url = urlChars ? std::string(urlChars) : std::string("segment");
|
|
652
|
-
if (urlChars) env->ReleaseStringUTFChars(sourceURL, urlChars);
|
|
653
|
-
|
|
654
|
-
// Fail-fast on THIS thread: settle exactly once, no registry entry created.
|
|
655
|
-
auto finishOnThisThread = [&](const std::string &err) {
|
|
656
|
-
settleBgEval(globalCallback, settled, err);
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
if (path.empty()) {
|
|
660
|
-
finishOnThisThread("IO_ERROR:Empty segment path");
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Snapshot the bg RuntimeExecutor under the timer mutex (the same lock that
|
|
665
|
-
// guards its assignment/teardown). If it's null the bg runtime hasn't
|
|
666
|
-
// installed its SharedBridge yet → retryable NO_RUNTIME.
|
|
667
|
-
RPCRuntimeExecutor executor;
|
|
668
|
-
{
|
|
669
|
-
std::lock_guard<std::mutex> lock(gTimerMutex);
|
|
670
|
-
executor = gBgTimerExecutor;
|
|
671
|
-
}
|
|
672
|
-
if (!executor) {
|
|
673
|
-
finishOnThisThread("NO_RUNTIME:Background runtime executor not available");
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// F: read the segment file HERE, on the calling (native module) thread,
|
|
678
|
-
// BEFORE dispatch — so only evaluateJavaScript + completion run on the bg JS
|
|
679
|
-
// thread and the read does not block it or race the watchdog.
|
|
680
|
-
std::string source;
|
|
681
|
-
if (!readBgFileToString(path, source)) {
|
|
682
|
-
finishOnThisThread("IO_ERROR:Failed to read bg segment file: " + path);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
if (source.empty()) {
|
|
686
|
-
finishOnThisThread("IO_ERROR:Empty bg segment file: " + path);
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Register this in-flight eval BEFORE dispatch so that if the enqueued work
|
|
691
|
-
// never runs (schedule fails, context==null, ptr==0, or nativeDestroy drops
|
|
692
|
-
// pending work) the drain can settle it as a retryable NO_RUNTIME failure
|
|
693
|
-
// and release the global ref — instead of leaking it and leaning on the
|
|
694
|
-
// Kotlin 30s watchdog. The work lambda removes its own entry when it runs.
|
|
695
|
-
int64_t bgEvalId;
|
|
696
|
-
{
|
|
697
|
-
std::lock_guard<std::mutex> lock(gBgEvalMutex);
|
|
698
|
-
bgEvalId = gNextBgEvalId++;
|
|
699
|
-
gPendingBgEvals[bgEvalId] = PendingBgEval{globalCallback, settled};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Move the already-read buffer + the global callback ref into the work
|
|
703
|
-
// lambda. The lambda runs on the bg JS thread (via nativeExecuteWork), which
|
|
704
|
-
// also drainMicrotasks() after — preserving eval+resolve as one atomic turn.
|
|
705
|
-
executor([globalCallback, settled, bgEvalId, source = std::move(source),
|
|
706
|
-
url = std::move(url)](jsi::Runtime &rt) {
|
|
707
|
-
// We are running now → claim ownership and remove our registry entry so
|
|
708
|
-
// a concurrent nativeDestroy drain can't also touch this eval.
|
|
709
|
-
{
|
|
710
|
-
std::lock_guard<std::mutex> lock(gBgEvalMutex);
|
|
711
|
-
gPendingBgEvals.erase(bgEvalId);
|
|
712
|
-
}
|
|
713
|
-
std::string error;
|
|
714
|
-
try {
|
|
715
|
-
LOGI("[SplitBundle] bg evaluating segment %s (%zu bytes)", url.c_str(),
|
|
716
|
-
source.size());
|
|
717
|
-
auto buffer =
|
|
718
|
-
std::make_shared<jsi::StringBuffer>(std::move(source));
|
|
719
|
-
// Runs the segment's top-level __d(...) synchronously on the bg JS
|
|
720
|
-
// thread before returning.
|
|
721
|
-
rt.evaluateJavaScript(std::move(buffer), url);
|
|
722
|
-
LOGI("[SplitBundle] bg segment %s evaluated", url.c_str());
|
|
723
|
-
} catch (const jsi::JSError &e) {
|
|
724
|
-
error = std::string("Bg segment eval JSError for ") + url + ": " +
|
|
725
|
-
e.getMessage();
|
|
726
|
-
LOGE("[SplitBundle] %s", error.c_str());
|
|
727
|
-
} catch (const std::exception &e) {
|
|
728
|
-
error = std::string("Bg segment eval failed for ") + url + ": " +
|
|
729
|
-
e.what();
|
|
730
|
-
LOGE("[SplitBundle] %s", error.c_str());
|
|
731
|
-
} catch (...) {
|
|
732
|
-
error = std::string("Bg segment eval failed for ") + url +
|
|
733
|
-
" (unknown C++ exception)";
|
|
734
|
-
LOGE("[SplitBundle] %s", error.c_str());
|
|
735
|
-
}
|
|
736
|
-
// Resolve/reject from INSIDE this same bg-JS-thread block, strictly
|
|
737
|
-
// AFTER eval above — the ordering guarantee that fixes the race. The
|
|
738
|
-
// shared `settled` flag makes this a no-op if a drain already claimed
|
|
739
|
-
// it (it would not have, since we erased our entry above, but the flag
|
|
740
|
-
// keeps the invariant airtight against any reorder).
|
|
741
|
-
settleBgEval(globalCallback, settled, error);
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// nativeDropScheduledWork: clean up after a scheduleOnJSThread drop path where
|
|
746
|
-
// CallVoidMethod itself SUCCEEDED but Kotlin then found the bg runtime
|
|
747
|
-
// unreachable (context==null / ptr==0) and returned WITHOUT calling
|
|
748
|
-
// nativeExecuteWork for this `workId`. Two things must be released:
|
|
749
|
-
// 1. gPendingWork[workId] — the stored work lambda (holding the segment
|
|
750
|
-
// SOURCE BUFFER). nativeExecuteWork is the only other eraser and it will
|
|
751
|
-
// never run for this id, so without this it leaks until nativeDestroy.
|
|
752
|
-
// 2. The in-flight bg eval(s) — settle as retryable NO_RUNTIME so the JNI
|
|
753
|
-
// global ref is released and the JS promise resolves now instead of
|
|
754
|
-
// hanging on the 30s watchdog. drain-all is sound: an unreachable bg JS
|
|
755
|
-
// thread dooms every enqueued bg eval equally.
|
|
756
|
-
// Exactly-once via the shared `settled` flag, so a recovered runtime that later
|
|
757
|
-
// DOES run stale work (it can't — we erased it) would be a harmless no-op.
|
|
758
|
-
extern "C" JNIEXPORT void JNICALL
|
|
759
|
-
Java_com_backgroundthread_BackgroundThreadManager_nativeDropScheduledWork(
|
|
760
|
-
JNIEnv * /* env */, jobject /* thiz */, jlong workId) {
|
|
761
|
-
{
|
|
762
|
-
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
763
|
-
gPendingWork.erase(static_cast<int64_t>(workId));
|
|
764
|
-
}
|
|
765
|
-
drainPendingBgEvals("Background runtime unreachable when scheduling segment eval");
|
|
766
|
-
}
|
|
767
|
-
|
|
768
610
|
// ── nativeInstallSharedBridge ───────────────────────────────────────────
|
|
769
611
|
// Install SharedStore and SharedRPC into a runtime.
|
|
770
612
|
extern "C" JNIEXPORT void JNICALL
|
|
@@ -788,71 +630,7 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
|
|
|
788
630
|
bool capturedIsMain = static_cast<bool>(isMain);
|
|
789
631
|
|
|
790
632
|
RPCRuntimeExecutor executor = [ref, capturedIsMain](std::function<void(jsi::Runtime &)> work) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
// Settle any enqueued-but-now-unrunnable bg eval when this work will NOT
|
|
794
|
-
// reach nativeExecuteWork. Bg eval lambdas are only ever dispatched via
|
|
795
|
-
// the bg executor, so this is gated on !capturedIsMain — a main-thread
|
|
796
|
-
// schedule hiccup must never falsely reject healthy bg evals. This
|
|
797
|
-
// mirrors the existing context==null / ptr==0 (nativeDropScheduledWork)
|
|
798
|
-
// and nativeDestroy drop paths: drain-all is sound because a failed bg
|
|
799
|
-
// schedule means the bg JS thread is unreachable, so every enqueued bg
|
|
800
|
-
// eval is equally doomed. NO_RUNTIME is retryable, so JS re-attempts.
|
|
801
|
-
auto drainBgEvalsIfBg = [capturedIsMain](const char *reason) {
|
|
802
|
-
if (!capturedIsMain) {
|
|
803
|
-
drainPendingBgEvals(reason);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
if (!env || !ref) {
|
|
808
|
-
LOGE("executor: env=%p, ref=%p — aborting", env, ref.get());
|
|
809
|
-
drainBgEvalsIfBg("Background executor env/ref unavailable when scheduling segment eval");
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
int64_t workId;
|
|
814
|
-
{
|
|
815
|
-
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
816
|
-
workId = gNextWorkId++;
|
|
817
|
-
gPendingWork[workId] = std::move(work);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
jclass cls = env->GetObjectClass(ref.get());
|
|
821
|
-
jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)V");
|
|
822
|
-
bool scheduled = false;
|
|
823
|
-
if (mid) {
|
|
824
|
-
LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", capturedIsMain, (long)workId);
|
|
825
|
-
env->CallVoidMethod(ref.get(), mid, static_cast<jboolean>(capturedIsMain), static_cast<jlong>(workId));
|
|
826
|
-
if (env->ExceptionCheck()) {
|
|
827
|
-
LOGE("executor: JNI exception after scheduleOnJSThread");
|
|
828
|
-
env->ExceptionDescribe();
|
|
829
|
-
env->ExceptionClear();
|
|
830
|
-
} else {
|
|
831
|
-
scheduled = true;
|
|
832
|
-
}
|
|
833
|
-
} else {
|
|
834
|
-
LOGE("executor: scheduleOnJSThread method not found!");
|
|
835
|
-
if (env->ExceptionCheck()) {
|
|
836
|
-
env->ExceptionDescribe();
|
|
837
|
-
env->ExceptionClear();
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
env->DeleteLocalRef(cls);
|
|
841
|
-
|
|
842
|
-
// Schedule failed (JNI exception or missing method): nativeExecuteWork
|
|
843
|
-
// will never run this workId, so erase it now to free the stored work
|
|
844
|
-
// (and its captured segment source buffer) instead of leaking it until
|
|
845
|
-
// nativeDestroy. Then settle the corresponding bg eval(s) so the JNI
|
|
846
|
-
// global ref is released and the JS promise resolves immediately rather
|
|
847
|
-
// than hanging on the Kotlin 30s watchdog. Erasing also makes a late
|
|
848
|
-
// Kotlin enqueue (if scheduleOnJSThread threw AFTER posting) a no-op.
|
|
849
|
-
if (!scheduled) {
|
|
850
|
-
{
|
|
851
|
-
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
852
|
-
gPendingWork.erase(workId);
|
|
853
|
-
}
|
|
854
|
-
drainBgEvalsIfBg("Background JS thread unreachable when scheduling segment eval");
|
|
855
|
-
}
|
|
633
|
+
enqueueRuntimeWork(ref, capturedIsMain, std::move(work));
|
|
856
634
|
};
|
|
857
635
|
|
|
858
636
|
std::string runtimeId = isMain ? "main" : "background";
|
|
@@ -998,24 +776,20 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy(
|
|
|
998
776
|
// Drain pending cross-runtime work. Each std::function may capture a
|
|
999
777
|
// shared_ptr<jsi::Function> tied to the destroyed runtime; leak them
|
|
1000
778
|
// for the same reason as above.
|
|
1001
|
-
//
|
|
1002
|
-
// NOTE: the bg-eval work lambdas captured here also hold a JNI global ref
|
|
1003
|
-
// to a SegmentEvalCallback. Leaking the std::function leaks that ref AND
|
|
1004
|
-
// leaves the JS promise pending — so we settle those explicitly below via
|
|
1005
|
-
// gPendingBgEvals (the entry survives independently of the leaked lambda).
|
|
1006
779
|
{
|
|
1007
780
|
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
1008
781
|
for (auto &entry : gPendingWork) {
|
|
1009
782
|
new std::function<void(jsi::Runtime &)>(std::move(entry.second));
|
|
1010
783
|
}
|
|
1011
784
|
gPendingWork.clear();
|
|
785
|
+
for (auto *queue : {&gMainRuntimeWorkQueue, &gBgRuntimeWorkQueue}) {
|
|
786
|
+
for (auto &work : queue->items) {
|
|
787
|
+
new std::function<void(jsi::Runtime &)>(std::move(work));
|
|
788
|
+
}
|
|
789
|
+
queue->items.clear();
|
|
790
|
+
queue->drainScheduled = false;
|
|
791
|
+
}
|
|
1012
792
|
}
|
|
1013
793
|
|
|
1014
|
-
// Drain pending bg-eval callbacks: the bg runtime is gone, so any eval that
|
|
1015
|
-
// was enqueued but never ran must be settled NOW (retryable NO_RUNTIME) so
|
|
1016
|
-
// the JS promise resolves immediately and the global ref is released —
|
|
1017
|
-
// rather than leaking and relying on the Kotlin 30s watchdog.
|
|
1018
|
-
drainPendingBgEvals("Background runtime destroyed before segment eval ran");
|
|
1019
|
-
|
|
1020
794
|
LOGI("Native resources cleaned up");
|
|
1021
795
|
}
|
|
@@ -24,7 +24,6 @@ import com.facebook.react.shell.MainReactPackage
|
|
|
24
24
|
import java.io.File
|
|
25
25
|
import java.lang.ref.WeakReference
|
|
26
26
|
import java.util.concurrent.TimeUnit
|
|
27
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Singleton manager for the background React Native runtime.
|
|
@@ -64,12 +63,6 @@ class BackgroundThreadManager private constructor() {
|
|
|
64
63
|
companion object {
|
|
65
64
|
private const val MODULE_NAME = "background"
|
|
66
65
|
|
|
67
|
-
// Bounded watchdog: if the bg JS thread never drains to our scheduled
|
|
68
|
-
// eval (e.g. the bg entry bundle never finished evaluating), reject as a
|
|
69
|
-
// RETRYABLE timeout rather than leaving the JS promise pending forever.
|
|
70
|
-
// Matches the main-runtime SplitBundleLoader watchdog.
|
|
71
|
-
private const val BG_SEGMENT_EVAL_TIMEOUT_MS = 30_000L
|
|
72
|
-
|
|
73
66
|
init {
|
|
74
67
|
System.loadLibrary("background_thread")
|
|
75
68
|
}
|
|
@@ -85,20 +78,6 @@ class BackgroundThreadManager private constructor() {
|
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
/**
|
|
89
|
-
* Completion contract invoked by the native (JNI) side AFTER the bg segment
|
|
90
|
-
* has been evaluated into the BACKGROUND runtime. Called from the bg JS
|
|
91
|
-
* thread (or synchronously on the caller thread for fail-fast paths).
|
|
92
|
-
*
|
|
93
|
-
* @param error null on success; a non-empty message on failure. A message
|
|
94
|
-
* prefixed with "NO_RUNTIME:" means the bg runtime is not ready yet
|
|
95
|
-
* (retryable); "IO_ERROR:" means the segment file read failed (fatal);
|
|
96
|
-
* any other message is a segment JS/Hermes eval throw (fatal).
|
|
97
|
-
*/
|
|
98
|
-
fun interface SegmentEvalCallback {
|
|
99
|
-
fun onComplete(error: String?)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
81
|
// ── JNI declarations ────────────────────────────────────────────────────
|
|
103
82
|
|
|
104
83
|
private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean)
|
|
@@ -106,34 +85,6 @@ class BackgroundThreadManager private constructor() {
|
|
|
106
85
|
private external fun nativeDestroy()
|
|
107
86
|
private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
|
|
108
87
|
|
|
109
|
-
/**
|
|
110
|
-
* Evaluate the segment at [segmentPath] into the BACKGROUND runtime on its
|
|
111
|
-
* JS thread and invoke [callback] from inside that same JS-thread block,
|
|
112
|
-
* strictly AFTER the segment's `__d(...)` module definitions have run. This
|
|
113
|
-
* is the ordering guarantee that fixes the bg "Requiring unknown module"
|
|
114
|
-
* race (the bg analogue of the main-runtime SplitBundleLoaderJSI fix).
|
|
115
|
-
*
|
|
116
|
-
* Returns immediately; [callback] fires later on the bg JS thread (or
|
|
117
|
-
* synchronously on the calling thread for fail-fast paths such as the bg
|
|
118
|
-
* runtime not being ready or the segment file failing to read).
|
|
119
|
-
*/
|
|
120
|
-
private external fun nativeEvaluateSegmentInBackground(
|
|
121
|
-
segmentPath: String,
|
|
122
|
-
sourceURL: String,
|
|
123
|
-
callback: SegmentEvalCallback
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Settle every in-flight bg segment eval as a retryable NO_RUNTIME failure
|
|
128
|
-
* without tearing the runtime down. Called from [scheduleOnJSThread] when
|
|
129
|
-
* the bg runtime is momentarily unreachable (context == null / ptr == 0) so
|
|
130
|
-
* any eval enqueued onto the native pending-work map — which will never be
|
|
131
|
-
* drained on the JS thread in that state — releases its JNI global ref and
|
|
132
|
-
* settles the JS promise immediately, instead of leaking until the next
|
|
133
|
-
* teardown or relying on the bg watchdog. Exactly-once on the native side.
|
|
134
|
-
*/
|
|
135
|
-
private external fun nativeDropScheduledWork(workId: Long)
|
|
136
|
-
|
|
137
88
|
/**
|
|
138
89
|
* Synchronously mark the SharedRPC listener for `runtimeId` as dead
|
|
139
90
|
* before the underlying JS runtime is torn down. See
|
|
@@ -456,140 +407,87 @@ class BackgroundThreadManager private constructor() {
|
|
|
456
407
|
* Routes to main or background runtime's JS queue thread, then calls nativeExecuteWork.
|
|
457
408
|
*/
|
|
458
409
|
@DoNotStrip
|
|
459
|
-
fun scheduleOnJSThread(isMain: Boolean, workId: Long) {
|
|
410
|
+
fun scheduleOnJSThread(isMain: Boolean, workId: Long): Boolean {
|
|
460
411
|
val context = if (isMain) mainReactContext else bgReactHost?.currentReactContext
|
|
461
412
|
BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}")
|
|
462
413
|
if (context == null) {
|
|
463
414
|
BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}")
|
|
464
|
-
|
|
465
|
-
// Drop it now: erase gPendingWork[workId] (frees the captured segment
|
|
466
|
-
// source buffer) and, if it was a bg segment eval, settle it
|
|
467
|
-
// (retryable NO_RUNTIME) so its JNI global ref is released and the JS
|
|
468
|
-
// promise resolves instead of leaking until teardown / the bg watchdog.
|
|
469
|
-
if (!isMain) {
|
|
470
|
-
nativeDropScheduledWork(workId)
|
|
471
|
-
}
|
|
472
|
-
return
|
|
415
|
+
return false
|
|
473
416
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// (stale/torn-down) bg runtime. Drop gPendingWork[workId] (frees
|
|
489
|
-
// the source buffer) and settle any pending bg eval so it doesn't
|
|
490
|
-
// leak its global ref / hang the JS promise.
|
|
491
|
-
if (!isMain) {
|
|
492
|
-
nativeDropScheduledWork(workId)
|
|
417
|
+
return try {
|
|
418
|
+
val posted = context.runOnJSQueueThread {
|
|
419
|
+
// Re-read ptr inside the block — if a reload happened between
|
|
420
|
+
// scheduling and execution, the old ptr may be stale.
|
|
421
|
+
val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr
|
|
422
|
+
BTLogger.info("scheduleOnJSThread runOnJSQueueThread: isMain=$isMain, workId=$workId, ptr=$ptr")
|
|
423
|
+
if (ptr != 0L) {
|
|
424
|
+
try {
|
|
425
|
+
nativeExecuteWork(ptr, workId)
|
|
426
|
+
} catch (e: Exception) {
|
|
427
|
+
BTLogger.error("Error executing work on JS thread: ${e.message}")
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
BTLogger.error("scheduleOnJSThread: ptr is 0! isMain=$isMain")
|
|
493
431
|
}
|
|
494
432
|
}
|
|
433
|
+
if (!posted) {
|
|
434
|
+
BTLogger.error("scheduleOnJSThread: runOnJSQueueThread rejected workId=$workId isMain=$isMain")
|
|
435
|
+
}
|
|
436
|
+
posted
|
|
437
|
+
} catch (e: Exception) {
|
|
438
|
+
BTLogger.error("scheduleOnJSThread: failed to post workId=$workId isMain=$isMain error=${e.message}")
|
|
439
|
+
false
|
|
495
440
|
}
|
|
496
441
|
}
|
|
497
442
|
|
|
498
443
|
// ── Segment Registration (Phase 2.5 spike) ─────────────────────────────
|
|
499
444
|
|
|
500
445
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
446
|
+
* Register a HBC segment in the background runtime.
|
|
447
|
+
* Uses CatalystInstance.registerSegment() on the background ReactContext.
|
|
503
448
|
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
*
|
|
511
|
-
*
|
|
449
|
+
* @param segmentId The segment ID to register
|
|
450
|
+
* @param path Absolute file path to the .seg.hbc file
|
|
451
|
+
* @throws IllegalStateException if background runtime is not started
|
|
452
|
+
* @throws IllegalArgumentException if segment file does not exist
|
|
453
|
+
*/
|
|
454
|
+
/**
|
|
455
|
+
* Register a HBC segment in the background runtime with completion callback.
|
|
456
|
+
* Dispatches to the background JS queue thread and invokes the callback
|
|
457
|
+
* only after registerSegment has actually executed.
|
|
512
458
|
*
|
|
513
|
-
* @param segmentId The segment ID
|
|
459
|
+
* @param segmentId The segment ID to register
|
|
514
460
|
* @param path Absolute file path to the .seg.hbc file
|
|
515
|
-
* @param onComplete Called with
|
|
516
|
-
* (code=<contract reject code>, message) on failure. The code is one of
|
|
517
|
-
* the SHARED split-bundle contract codes so the JS loader's retryable set
|
|
518
|
-
* { SPLIT_BUNDLE_NO_RUNTIME, SPLIT_BUNDLE_TIMEOUT } classifies correctly.
|
|
461
|
+
* @param onComplete Called with null on success, or an Exception on failure
|
|
519
462
|
*/
|
|
520
|
-
fun registerSegmentInBackground(
|
|
521
|
-
segmentId: Int,
|
|
522
|
-
path: String,
|
|
523
|
-
onComplete: (code: String?, message: String?) -> Unit
|
|
524
|
-
) {
|
|
463
|
+
fun registerSegmentInBackground(segmentId: Int, path: String, onComplete: (Exception?) -> Unit) {
|
|
525
464
|
if (!isStarted) {
|
|
526
|
-
|
|
527
|
-
// once the bg host is up).
|
|
528
|
-
onComplete("SPLIT_BUNDLE_NO_RUNTIME", "Background runtime not started")
|
|
465
|
+
onComplete(IllegalStateException("Background runtime not started"))
|
|
529
466
|
return
|
|
530
467
|
}
|
|
531
468
|
|
|
532
469
|
val file = File(path)
|
|
533
470
|
if (!file.exists()) {
|
|
534
|
-
onComplete("
|
|
471
|
+
onComplete(IllegalArgumentException("Segment file not found: $path"))
|
|
535
472
|
return
|
|
536
473
|
}
|
|
537
474
|
|
|
538
475
|
val context = bgReactHost?.currentReactContext
|
|
539
476
|
if (context == null) {
|
|
540
|
-
onComplete("
|
|
477
|
+
onComplete(IllegalStateException("Background ReactContext not available"))
|
|
541
478
|
return
|
|
542
479
|
}
|
|
543
480
|
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
val settled = AtomicBoolean(false)
|
|
547
|
-
val sourceURL = "seg-$segmentId.js"
|
|
548
|
-
val segStart = System.nanoTime()
|
|
549
|
-
|
|
550
|
-
// Bounded watchdog: if the bg JS thread never drains to our eval, reject
|
|
551
|
-
// with the RETRYABLE SPLIT_BUNDLE_TIMEOUT instead of hanging forever.
|
|
552
|
-
val watchdog = Handler(Looper.getMainLooper())
|
|
553
|
-
val timeoutRunnable = Runnable {
|
|
554
|
-
if (settled.compareAndSet(false, true)) {
|
|
555
|
-
BTLogger.error("[SplitBundle] bg segment id=$segmentId eval timed out after ${BG_SEGMENT_EVAL_TIMEOUT_MS}ms (bg entry bundle likely never finished evaluating); rejecting as retryable timeout")
|
|
556
|
-
onComplete("SPLIT_BUNDLE_TIMEOUT", "Bg segment eval timed out: id=$segmentId")
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
watchdog.postDelayed(timeoutRunnable, BG_SEGMENT_EVAL_TIMEOUT_MS)
|
|
560
|
-
|
|
481
|
+
// Use ReactContext.registerSegment which works in both bridge
|
|
482
|
+
// and bridgeless modes.
|
|
561
483
|
try {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (error == null) {
|
|
566
|
-
val segMs = (System.nanoTime() - segStart) / 1_000_000.0
|
|
567
|
-
BTLogger.info("[SplitBundle] bg segment id=$segmentId evaluated in ${String.format("%.1f", segMs)}ms (eval-complete)")
|
|
568
|
-
onComplete(null, null)
|
|
569
|
-
} else {
|
|
570
|
-
// Native prefixes its failures so we can map to the
|
|
571
|
-
// shared contract codes: NO_RUNTIME (retryable),
|
|
572
|
-
// IO_ERROR (fatal), else eval throw (fatal).
|
|
573
|
-
when {
|
|
574
|
-
error.startsWith("NO_RUNTIME:") ->
|
|
575
|
-
onComplete("SPLIT_BUNDLE_NO_RUNTIME", error.removePrefix("NO_RUNTIME:"))
|
|
576
|
-
error.startsWith("IO_ERROR:") ->
|
|
577
|
-
onComplete("SPLIT_BUNDLE_IO_ERROR", error.removePrefix("IO_ERROR:"))
|
|
578
|
-
else ->
|
|
579
|
-
onComplete("SPLIT_BUNDLE_EVAL_ERROR", error)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
} catch (e: Throwable) {
|
|
585
|
-
// nativeEvaluateSegmentInBackground itself failed to dispatch (e.g.
|
|
586
|
-
// UnsatisfiedLinkError). Fail closed; do NOT fall back to the
|
|
587
|
-
// race-prone registerSegment path.
|
|
588
|
-
if (settled.compareAndSet(false, true)) {
|
|
589
|
-
watchdog.removeCallbacks(timeoutRunnable)
|
|
590
|
-
BTLogger.error("[SplitBundle] FATAL: nativeEvaluateSegmentInBackground threw for id=$segmentId: ${e.message}")
|
|
591
|
-
onComplete("SPLIT_BUNDLE_NATIVE_UNAVAILABLE", "Bg native segment eval unavailable: ${e.message}")
|
|
484
|
+
context.registerSegment(segmentId, path) {
|
|
485
|
+
BTLogger.info("Segment registered in background runtime: id=$segmentId, path=$path")
|
|
486
|
+
onComplete(null)
|
|
592
487
|
}
|
|
488
|
+
} catch (e: Exception) {
|
|
489
|
+
BTLogger.error("Failed to register segment in background runtime: ${e.message}")
|
|
490
|
+
onComplete(e)
|
|
593
491
|
}
|
|
594
492
|
}
|
|
595
493
|
|
|
@@ -30,17 +30,9 @@ class BackgroundThreadModule(reactContext: ReactApplicationContext) :
|
|
|
30
30
|
|
|
31
31
|
override fun loadSegmentInBackground(segmentId: Double, path: String, promise: Promise) {
|
|
32
32
|
BackgroundThreadManager.getInstance()
|
|
33
|
-
.registerSegmentInBackground(segmentId.toInt(), path) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
// SPLIT_BUNDLE_NO_RUNTIME / SPLIT_BUNDLE_TIMEOUT are
|
|
37
|
-
// retryable; SPLIT_BUNDLE_IO_ERROR / SPLIT_BUNDLE_EVAL_ERROR
|
|
38
|
-
// / SPLIT_BUNDLE_NATIVE_UNAVAILABLE / SPLIT_BUNDLE_NOT_FOUND
|
|
39
|
-
// are not) so the JS loader classifies retryability the same
|
|
40
|
-
// way it does for the main path. registerSegmentInBackground
|
|
41
|
-
// always supplies one of these contract codes here, so there
|
|
42
|
-
// is no legacy/opaque default string to fall back to.
|
|
43
|
-
promise.reject(code, message)
|
|
33
|
+
.registerSegmentInBackground(segmentId.toInt(), path) { error ->
|
|
34
|
+
if (error != null) {
|
|
35
|
+
promise.reject("BG_SEGMENT_LOAD_ERROR", error.message, error)
|
|
44
36
|
} else {
|
|
45
37
|
promise.resolve(null)
|
|
46
38
|
}
|
package/ios/BackgroundThread.mm
CHANGED
|
@@ -38,54 +38,7 @@
|
|
|
38
38
|
path:path
|
|
39
39
|
completion:^(NSError * _Nullable error) {
|
|
40
40
|
if (error) {
|
|
41
|
-
|
|
42
|
-
// reject code (matching SplitBundleLoader's main-runtime mapping and
|
|
43
|
-
// the shared error-code contract) so JS can classify
|
|
44
|
-
// retryable-vs-fatal. EVERY EBgMgrSegmentEvalError case is mapped
|
|
45
|
-
// EXPLICITLY here — there is no silent `default → NO_RUNTIME` that
|
|
46
|
-
// could MISCLASSIFY a fatal failure (e.g. a missing segment file)
|
|
47
|
-
// as a transient runtime-not-ready that gets retried/masked. The old
|
|
48
|
-
// code both collapsed bg failures into one opaque
|
|
49
|
-
// `BG_SEGMENT_LOAD_ERROR` and (before that) let raw codes 1/2 fall
|
|
50
|
-
// through `default` to retryable NO_RUNTIME.
|
|
51
|
-
NSString *rejectCode;
|
|
52
|
-
switch ((EBgMgrSegmentEvalError)error.code) {
|
|
53
|
-
case EBgMgrSegmentEvalErrorNotStarted:
|
|
54
|
-
case EBgMgrSegmentEvalErrorNilInstance:
|
|
55
|
-
// Bg runtime not started / RCTInstance nil — TRANSIENT: the
|
|
56
|
-
// bg host simply wasn't ready yet; a later attempt may win.
|
|
57
|
-
rejectCode = @"SPLIT_BUNDLE_NO_RUNTIME"; // retryable
|
|
58
|
-
break;
|
|
59
|
-
case EBgMgrSegmentEvalErrorFileNotFound:
|
|
60
|
-
// Segment file missing — FATAL packaging/OTA corruption.
|
|
61
|
-
rejectCode = @"SPLIT_BUNDLE_NOT_FOUND"; // fatal
|
|
62
|
-
break;
|
|
63
|
-
case EBgMgrSegmentEvalErrorIvarMissing:
|
|
64
|
-
// `_rctInstance` ivar reflection failed — STRUCTURAL/PERMANENT
|
|
65
|
-
// (an RN bump renamed the private field). Retrying is futile.
|
|
66
|
-
rejectCode = @"SPLIT_BUNDLE_NATIVE_UNAVAILABLE"; // fatal
|
|
67
|
-
break;
|
|
68
|
-
case EBgMgrSegmentEvalErrorIORead:
|
|
69
|
-
rejectCode = @"SPLIT_BUNDLE_IO_ERROR"; // fatal
|
|
70
|
-
break;
|
|
71
|
-
case EBgMgrSegmentEvalErrorEvalThrow:
|
|
72
|
-
rejectCode = @"SPLIT_BUNDLE_EVAL_ERROR"; // fatal (segment bug)
|
|
73
|
-
break;
|
|
74
|
-
case EBgMgrSegmentEvalErrorTimeout:
|
|
75
|
-
rejectCode = @"SPLIT_BUNDLE_TIMEOUT"; // retryable
|
|
76
|
-
break;
|
|
77
|
-
default:
|
|
78
|
-
// Defensive LAST RESORT only: every real EBgMgrSegmentEvalError
|
|
79
|
-
// case is handled above, so reaching here means the manager
|
|
80
|
-
// emitted an unmapped code (a bug). Choose retryable
|
|
81
|
-
// NO_RUNTIME so a stale/unknown native build degrades safely
|
|
82
|
-
// rather than permanently poisoning a segment — but log it
|
|
83
|
-
// loudly so the unmapped code is caught and named.
|
|
84
|
-
[BTLogger warn:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: UNMAPPED EBgMgrSegmentEvalError code=%ld — defaulting to retryable NO_RUNTIME. This is a native bug: add an explicit case.", (long)error.code]];
|
|
85
|
-
rejectCode = @"SPLIT_BUNDLE_NO_RUNTIME"; // retryable (defensive)
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
reject(rejectCode, error.localizedDescription, error);
|
|
41
|
+
reject(@"BG_SEGMENT_LOAD_ERROR", error.localizedDescription, error);
|
|
89
42
|
} else {
|
|
90
43
|
resolve(nil);
|
|
91
44
|
}
|
|
@@ -6,49 +6,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
6
6
|
@class RCTReactNativeFactory;
|
|
7
7
|
@class RCTHost;
|
|
8
8
|
|
|
9
|
-
/// NSError `code` values produced by `registerSegmentInBackground:` /
|
|
10
|
-
/// `evaluateSegmentInBackground:` under the `BackgroundThread` error domain.
|
|
11
|
-
///
|
|
12
|
-
/// These are DISTINCT (kept numerically aligned with SplitBundleLoader's
|
|
13
|
-
/// `ESegmentEvalError`) so the TurboModule boundary
|
|
14
|
-
/// (`BackgroundThread.loadSegmentInBackground`) can map each to its OWN JS
|
|
15
|
-
/// reject code and JS can classify retryable-vs-fatal — rather than collapsing
|
|
16
|
-
/// every bg failure into one opaque code (fix E). EVERY failure the manager
|
|
17
|
-
/// produces MUST have a NAMED case here so the boundary's switch maps it
|
|
18
|
-
/// explicitly: a raw integer literal would fall through to the boundary's
|
|
19
|
-
/// defensive `default` (→ retryable NO_RUNTIME) and silently MISCLASSIFY a
|
|
20
|
-
/// fatal failure (e.g. a missing segment file) as a transient one that gets
|
|
21
|
-
/// retried/masked instead of surfaced. The mapping the boundary applies is:
|
|
22
|
-
/// - NotStarted (bg runtime not started yet) → `SPLIT_BUNDLE_NO_RUNTIME` (retryable)
|
|
23
|
-
/// - FileNotFound (segment file missing) → `SPLIT_BUNDLE_NOT_FOUND` (fatal)
|
|
24
|
-
/// - NilInstance (bg RCTInstance is nil) → `SPLIT_BUNDLE_NO_RUNTIME` (retryable)
|
|
25
|
-
/// - IORead (file read/mmap failed) → `SPLIT_BUNDLE_IO_ERROR` (fatal)
|
|
26
|
-
/// - EvalThrow (segment JS/Hermes bug) → `SPLIT_BUNDLE_EVAL_ERROR` (fatal)
|
|
27
|
-
/// - Timeout (buffered executor never ran)→ `SPLIT_BUNDLE_TIMEOUT` (retryable)
|
|
28
|
-
/// - IvarMissing (`_rctInstance` reflection
|
|
29
|
-
/// failed — STRUCTURAL/permanent)→ `SPLIT_BUNDLE_NATIVE_UNAVAILABLE` (fatal)
|
|
30
|
-
///
|
|
31
|
-
/// WHY NilInstance and IvarMissing are split (and not both NO_RUNTIME): a nil
|
|
32
|
-
/// RCTInstance is TRANSIENT — the bg host just isn't up yet, a later attempt
|
|
33
|
-
/// can succeed. A missing `_rctInstance` ivar is STRUCTURAL/PERMANENT — an RN
|
|
34
|
-
/// version bump renamed/removed the private field our reflection depends on, so
|
|
35
|
-
/// bg segment loading is disabled until the native code is updated. Retrying
|
|
36
|
-
/// the latter is futile, so it maps to fatal NATIVE_UNAVAILABLE.
|
|
37
|
-
///
|
|
38
|
-
/// Declared here (not file-local in the .mm) so the boundary references these
|
|
39
|
-
/// by NAME — a future renumbering stays exact instead of drifting against a
|
|
40
|
-
/// hardcoded magic-number switch. Values 3-6 are kept STABLE; 1/2/7 fill in the
|
|
41
|
-
/// previously-raw `registerSegmentInBackground:` codes and the structural split.
|
|
42
|
-
typedef NS_ENUM(NSInteger, EBgMgrSegmentEvalError) {
|
|
43
|
-
EBgMgrSegmentEvalErrorNotStarted = 1, // bg runtime not started yet (retryable)
|
|
44
|
-
EBgMgrSegmentEvalErrorFileNotFound = 2, // segment file missing (fatal)
|
|
45
|
-
EBgMgrSegmentEvalErrorNilInstance = 3, // bg RCTInstance is nil (retryable)
|
|
46
|
-
EBgMgrSegmentEvalErrorIORead = 4, // file read/mmap failed (fatal)
|
|
47
|
-
EBgMgrSegmentEvalErrorEvalThrow = 5, // segment JS/Hermes bug (fatal)
|
|
48
|
-
EBgMgrSegmentEvalErrorTimeout = 6, // buffered executor never ran (retryable)
|
|
49
|
-
EBgMgrSegmentEvalErrorIvarMissing = 7, // `_rctInstance` ivar reflection failed (fatal, structural)
|
|
50
|
-
};
|
|
51
|
-
|
|
52
9
|
@interface BackgroundThreadManager : NSObject
|
|
53
10
|
|
|
54
11
|
/// Shared instance for singleton pattern
|
|
@@ -20,82 +20,6 @@
|
|
|
20
20
|
#import <React/RCTReloadCommand.h>
|
|
21
21
|
#import <ReactCommon/RCTHost.h>
|
|
22
22
|
#import <objc/runtime.h>
|
|
23
|
-
#import <os/lock.h>
|
|
24
|
-
#include <jsi/jsi.h>
|
|
25
|
-
#include <cstdint>
|
|
26
|
-
|
|
27
|
-
namespace {
|
|
28
|
-
|
|
29
|
-
// Zero-copy jsi::Buffer that retains its NSData for the async (buffered)
|
|
30
|
-
// executor block's lifetime — same rationale as SplitBundleLoader's
|
|
31
|
-
// NSDataJSIBuffer (M4/M5): no second full copy of the segment bytes, and the
|
|
32
|
-
// mmap'd/heap bytes stay alive because the buffer owns the NSData.
|
|
33
|
-
class BgMgrNSDataJSIBuffer : public facebook::jsi::Buffer {
|
|
34
|
-
public:
|
|
35
|
-
explicit BgMgrNSDataJSIBuffer(NSData *data) : data_(data) {}
|
|
36
|
-
size_t size() const override { return data_.length; }
|
|
37
|
-
const uint8_t *data() const override {
|
|
38
|
-
return static_cast<const uint8_t *>(data_.bytes);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private:
|
|
42
|
-
NSData *data_; // strong retain (ARC).
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
} // namespace
|
|
46
|
-
|
|
47
|
-
// Exactly-once settle guard for the bg watchdog — same design as
|
|
48
|
-
// SplitBundleLoader's SBLSettleGuard. An ARC object captured by both the
|
|
49
|
-
// executor block and the watchdog dispatch_after; ARC keeps its lock alive
|
|
50
|
-
// until both release, so neither block needs (or may do) a manual free —
|
|
51
|
-
// avoiding the use-after-free that would occur if either freed the lock while
|
|
52
|
-
// the other still runs.
|
|
53
|
-
@interface BgMgrSettleGuard : NSObject
|
|
54
|
-
- (BOOL)tryClaim;
|
|
55
|
-
@end
|
|
56
|
-
|
|
57
|
-
@implementation BgMgrSettleGuard {
|
|
58
|
-
os_unfair_lock _lock;
|
|
59
|
-
BOOL _settled;
|
|
60
|
-
}
|
|
61
|
-
- (instancetype)init {
|
|
62
|
-
if (self = [super init]) {
|
|
63
|
-
_lock = OS_UNFAIR_LOCK_INIT;
|
|
64
|
-
_settled = NO;
|
|
65
|
-
}
|
|
66
|
-
return self;
|
|
67
|
-
}
|
|
68
|
-
- (BOOL)tryClaim {
|
|
69
|
-
os_unfair_lock_lock(&_lock);
|
|
70
|
-
BOOL won = !_settled;
|
|
71
|
-
if (won) {
|
|
72
|
-
_settled = YES;
|
|
73
|
-
}
|
|
74
|
-
os_unfair_lock_unlock(&_lock);
|
|
75
|
-
return won;
|
|
76
|
-
}
|
|
77
|
-
@end
|
|
78
|
-
|
|
79
|
-
// Watchdog window for the bg buffered runtime executor (H2 = bg-runtime port of
|
|
80
|
-
// SplitBundleLoader's C1). The bg runtime's buffered executor stays buffered
|
|
81
|
-
// until the bg entry bundle finishes evaluating in
|
|
82
|
-
// BackgroundReactNativeDelegate.hostDidStart:; if that never completes (host
|
|
83
|
-
// teardown, OTA-resolve abort), the block never runs — without this watchdog
|
|
84
|
-
// the JS promise would hang inflightSegments forever.
|
|
85
|
-
//
|
|
86
|
-
// Fix G: 30s (matching Android and the main-runtime watchdog). The buffered
|
|
87
|
-
// executor stays buffered until the bg ENTRY bundle finishes evaluating; on a
|
|
88
|
-
// slow/throttled cold start that entry eval can itself exceed 10s, which would
|
|
89
|
-
// falsely trip the watchdog on a load that was about to succeed. 30s keeps the
|
|
90
|
-
// genuine-wedge safety net while leaving generous headroom for slow cold starts.
|
|
91
|
-
static const NSTimeInterval kBgSegmentEvalWatchdogSeconds = 30.0;
|
|
92
|
-
|
|
93
|
-
// EBgMgrSegmentEvalError NSError `code` values are declared in
|
|
94
|
-
// BackgroundThreadManager.h so the TurboModule boundary
|
|
95
|
-
// (BackgroundThread.loadSegmentInBackground) can map each distinct code to its
|
|
96
|
-
// own JS reject code by NAME. They stay numerically aligned with
|
|
97
|
-
// SplitBundleLoader's ESegmentEvalError; see installProdBundleLoader.ts for the
|
|
98
|
-
// retryable-vs-fatal classification (H3 / fix E).
|
|
99
23
|
|
|
100
24
|
@interface BackgroundThreadManager ()
|
|
101
25
|
@property (nonatomic, strong) BackgroundReactNativeDelegate *reactNativeFactoryDelegate;
|
|
@@ -277,166 +201,33 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
|
|
|
277
201
|
completion:(void (^)(NSError * _Nullable error))completion
|
|
278
202
|
{
|
|
279
203
|
if (!self.isStarted || !self.reactNativeFactoryDelegate) {
|
|
280
|
-
// Transient: the bg runtime just isn't up yet. NotStarted → NO_RUNTIME
|
|
281
|
-
// (retryable). Previously a raw `code:1` which fell through the
|
|
282
|
-
// boundary's `default` — same destination, but now NAMED so the mapping
|
|
283
|
-
// is explicit and can never drift (fix 1 / E).
|
|
284
204
|
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
285
|
-
code:
|
|
205
|
+
code:1
|
|
286
206
|
userInfo:@{NSLocalizedDescriptionKey: @"Background runtime not started"}];
|
|
287
207
|
if (completion) completion(error);
|
|
288
208
|
return;
|
|
289
209
|
}
|
|
290
210
|
|
|
291
|
-
// Verify the file exists
|
|
292
|
-
// OTA corruption — retrying just re-misses. Previously a raw `code:2` that
|
|
293
|
-
// the boundary's `default` MISCLASSIFIED as retryable NO_RUNTIME, masking
|
|
294
|
-
// the corruption; now FileNotFound → NOT_FOUND (fatal) (fix 1 / E — the
|
|
295
|
-
// NO-SHIP blocker).
|
|
211
|
+
// Verify the file exists
|
|
296
212
|
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
297
213
|
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
298
|
-
code:
|
|
214
|
+
code:2
|
|
299
215
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
300
216
|
[NSString stringWithFormat:@"Segment file not found: %@", path]}];
|
|
301
217
|
if (completion) completion(error);
|
|
302
218
|
return;
|
|
303
219
|
}
|
|
304
220
|
|
|
305
|
-
[self
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
// WHY THIS REPLACES registerSegmentWithId: + immediate completion(nil):
|
|
311
|
-
// The old path called `[delegate registerSegmentWithId:path:]` (which routes to
|
|
312
|
-
// RCTInstance/ReactInstance::registerSegment — that only ENQUEUES
|
|
313
|
-
// `runtime.evaluateJavaScript(segment)` on the runtime scheduler and returns)
|
|
314
|
-
// then resolved the JS promise immediately. That is the exact
|
|
315
|
-
// "Requiring unknown module" race the MAIN runtime already fixed in
|
|
316
|
-
// SplitBundleLoader: Metro's `import().then(() => __r(moduleId))` microtask can
|
|
317
|
-
// run `__r` BEFORE the scheduled eval populated the module table → a FATAL,
|
|
318
|
-
// uncatchable crash. Locale segments load through THIS path in the bg runtime
|
|
319
|
-
// (ServiceSetting.refreshLocaleMessages → import('./json/*.json') runs in
|
|
320
|
-
// kit-bg), so a language switch could still crash.
|
|
321
|
-
//
|
|
322
|
-
// Fix: evaluate the segment OURSELVES inside one
|
|
323
|
-
// `callFunctionOnBufferedRuntimeExecutor:` block on the bg RCTInstance and
|
|
324
|
-
// signal completion in that SAME block, strictly AFTER eval — making eval +
|
|
325
|
-
// resolve one atomic unit so any subsequent `__r(moduleId)` finds the module.
|
|
326
|
-
// The bg RCTInstance is the delegate's private `_rctInstance` ivar; we read it
|
|
327
|
-
// reflectively (the same pattern installSharedBridgeInMainRuntime: uses on the
|
|
328
|
-
// main host) to keep this fix self-contained in this file rather than widening
|
|
329
|
-
// the delegate's surface. Includes the same exactly-once + watchdog guard as
|
|
330
|
-
// the main-runtime fix (C1) because the bg buffered executor is likewise
|
|
331
|
-
// buffered until the bg entry bundle finishes evaluating in hostDidStart:.
|
|
332
|
-
- (void)evaluateSegmentInBackground:(NSNumber *)segmentId
|
|
333
|
-
path:(NSString *)path
|
|
334
|
-
completion:(void (^)(NSError * _Nullable error))completion
|
|
335
|
-
{
|
|
336
|
-
// Reach the bg RCTInstance via the delegate's `_rctInstance` ivar. `id`
|
|
337
|
-
// (not a typed RCTInstance*) mirrors installSharedBridgeInMainRuntime:'s
|
|
338
|
-
// untyped handling and avoids needing the RCTInstance header here.
|
|
339
|
-
BackgroundReactNativeDelegate *delegate = self.reactNativeFactoryDelegate;
|
|
340
|
-
Ivar ivar = class_getInstanceVariable([delegate class], "_rctInstance");
|
|
341
|
-
if (!ivar) {
|
|
342
|
-
// Loud failure (L7 parity): a future RN/delegate refactor that renames
|
|
343
|
-
// this ivar silently disables ALL bg segment loading — surface it.
|
|
344
|
-
// STRUCTURAL/PERMANENT (not transient): retrying can never recreate a
|
|
345
|
-
// renamed ivar, so this maps to fatal NATIVE_UNAVAILABLE — distinct from
|
|
346
|
-
// the nil-instance case below, which IS transient. Misclassifying this
|
|
347
|
-
// as NO_RUNTIME would make JS retry a permanently-broken reflection.
|
|
348
|
-
[BTLogger error:[NSString stringWithFormat:@"[SplitBundle] FATAL: _rctInstance ivar not found on %@ — bg segment loading is DISABLED.", [delegate class]]];
|
|
349
|
-
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
350
|
-
code:EBgMgrSegmentEvalErrorIvarMissing
|
|
351
|
-
userInfo:@{NSLocalizedDescriptionKey: @"_rctInstance ivar not found on bg delegate"}];
|
|
352
|
-
if (completion) completion(error);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
id instance = object_getIvar(delegate, ivar);
|
|
356
|
-
if (!instance) {
|
|
357
|
-
[BTLogger error:@"[SplitBundle] bg loadSegment: background RCTInstance not available"];
|
|
358
|
-
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
359
|
-
code:EBgMgrSegmentEvalErrorNilInstance
|
|
360
|
-
userInfo:@{NSLocalizedDescriptionKey: @"Background RCTInstance not available"}];
|
|
361
|
-
if (completion) completion(error);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// M4/M5: mmap + zero-copy buffer (retains the NSData for the async block).
|
|
366
|
-
NSError *readError = nil;
|
|
367
|
-
NSData *data = [NSData dataWithContentsOfFile:path
|
|
368
|
-
options:NSDataReadingMappedIfSafe
|
|
369
|
-
error:&readError];
|
|
370
|
-
if (!data || data.length == 0) {
|
|
221
|
+
BOOL success = [self.reactNativeFactoryDelegate registerSegmentWithId:segmentId path:path];
|
|
222
|
+
if (success) {
|
|
223
|
+
if (completion) completion(nil);
|
|
224
|
+
} else {
|
|
371
225
|
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
372
|
-
code:
|
|
373
|
-
userInfo:@{NSLocalizedDescriptionKey:
|
|
226
|
+
code:3
|
|
227
|
+
userInfo:@{NSLocalizedDescriptionKey:
|
|
228
|
+
@"Failed to register segment in background runtime"}];
|
|
374
229
|
if (completion) completion(error);
|
|
375
|
-
return;
|
|
376
230
|
}
|
|
377
|
-
|
|
378
|
-
// M6: synthetic `seg-<id>.js` source URL for in-segment crash symbolication.
|
|
379
|
-
int segIdInt = segmentId.intValue;
|
|
380
|
-
NSString *sourceURL = [NSString stringWithFormat:@"seg-%d.js", segIdInt];
|
|
381
|
-
|
|
382
|
-
// C1: exactly-once guard shared (and retained) by the executor block and the
|
|
383
|
-
// watchdog. ARC-owned so the lock outlives both with no manual free.
|
|
384
|
-
BgMgrSettleGuard *settleGuard = [[BgMgrSettleGuard alloc] init];
|
|
385
|
-
|
|
386
|
-
CFAbsoluteTime dispatchStart = CFAbsoluteTimeGetCurrent();
|
|
387
|
-
[BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: evaluating %@ (id=%d, %lu bytes)", sourceURL, segIdInt, (unsigned long)data.length]];
|
|
388
|
-
|
|
389
|
-
// No __weak dance needed here (unlike the SharedRPC executor): this block
|
|
390
|
-
// does not re-dispatch onto the instance — it runs synchronously inside the
|
|
391
|
-
// instance's own runtime executor with a live `runtime &`, so by the time it
|
|
392
|
-
// executes the instance is necessarily still alive.
|
|
393
|
-
[instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) {
|
|
394
|
-
@autoreleasepool {
|
|
395
|
-
BOOL won = [settleGuard tryClaim];
|
|
396
|
-
NSError *evalError = nil;
|
|
397
|
-
CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent();
|
|
398
|
-
try {
|
|
399
|
-
auto buffer = std::make_shared<BgMgrNSDataJSIBuffer>(data);
|
|
400
|
-
runtime.evaluateJavaScript(buffer, [sourceURL UTF8String]);
|
|
401
|
-
double evalMs = (CFAbsoluteTimeGetCurrent() - evalStart) * 1000.0;
|
|
402
|
-
[BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluated in %.1fms", sourceURL, evalMs]];
|
|
403
|
-
} catch (const std::exception &e) {
|
|
404
|
-
evalError = [NSError errorWithDomain:@"BackgroundThread"
|
|
405
|
-
code:EBgMgrSegmentEvalErrorEvalThrow
|
|
406
|
-
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment evaluation failed for %@: %s", sourceURL, e.what()]}];
|
|
407
|
-
[BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluation threw: %s", sourceURL, e.what()]];
|
|
408
|
-
} catch (...) {
|
|
409
|
-
evalError = [NSError errorWithDomain:@"BackgroundThread"
|
|
410
|
-
code:EBgMgrSegmentEvalErrorEvalThrow
|
|
411
|
-
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment evaluation failed for %@ (unknown C++ exception)", sourceURL]}];
|
|
412
|
-
[BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluation threw an unknown exception", sourceURL]];
|
|
413
|
-
}
|
|
414
|
-
if (won) {
|
|
415
|
-
// Resolve AFTER eval — the ordering guarantee that fixes the race.
|
|
416
|
-
if (completion) completion(evalError);
|
|
417
|
-
} else {
|
|
418
|
-
[BTLogger warn:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ evaluated AFTER watchdog already settled (bg entry was wedged >%.0fs)", sourceURL, kBgSegmentEvalWatchdogSeconds]];
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}];
|
|
422
|
-
|
|
423
|
-
// C1 watchdog — fires only on a genuine wedge (bg entry bundle never
|
|
424
|
-
// finished evaluating). Rejects with a retryable timeout so the JS loader
|
|
425
|
-
// re-attempts. settleGuard is retained by this block, so the lock stays
|
|
426
|
-
// valid even if the executor block later runs.
|
|
427
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kBgSegmentEvalWatchdogSeconds * NSEC_PER_SEC)),
|
|
428
|
-
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
429
|
-
if ([settleGuard tryClaim]) {
|
|
430
|
-
[BTLogger error:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ WATCHDOG fired after %.0fs — bg runtime executor never ran (bg entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, kBgSegmentEvalWatchdogSeconds]];
|
|
431
|
-
NSError *timeoutError = [NSError errorWithDomain:@"BackgroundThread"
|
|
432
|
-
code:EBgMgrSegmentEvalErrorTimeout
|
|
433
|
-
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Bg segment %@ eval timed out after %.0fs (buffered runtime executor never ran)", sourceURL, kBgSegmentEvalWatchdogSeconds]}];
|
|
434
|
-
if (completion) completion(timeoutError);
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
double dispatchMs = (CFAbsoluteTimeGetCurrent() - dispatchStart) * 1000.0;
|
|
439
|
-
[BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg loadSegment: %@ dispatched in %.1fms (resolve fires after eval; watchdog %.0fs)", sourceURL, dispatchMs, kBgSegmentEvalWatchdogSeconds]];
|
|
440
231
|
}
|
|
441
232
|
|
|
442
233
|
#pragma mark - Restart
|
package/package.json
CHANGED
package/src/SharedRPC.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface ISharedRPC {
|
|
|
4
4
|
// the payload value, so there is no read-back. `read`/`has`/`pendingCount`
|
|
5
5
|
// (the old slot-map introspection) are gone.
|
|
6
6
|
onWrite(
|
|
7
|
-
callback: (callId: string, value: string | number | boolean) => void
|
|
7
|
+
callback: (callId: string, value: string | number | boolean) => void
|
|
8
8
|
): void;
|
|
9
9
|
// The calling runtime declares which SharedStore key holds its readiness
|
|
10
10
|
// payload, so the native invalidate path clears exactly that key on teardown
|