@onekeyfe/react-native-background-thread 3.0.90 → 3.0.92

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.
@@ -70,6 +70,10 @@ static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name
70
70
  static std::mutex gWorkMutex;
71
71
  static std::unordered_map<int64_t, std::function<void(jsi::Runtime &)>> gPendingWork;
72
72
  static int64_t gNextWorkId = 0;
73
+ // Protected by gWorkMutex. Every background executor captures the generation
74
+ // that installed it, so work from an outgoing Hermes runtime can be discarded
75
+ // without ever touching its replacement runtime.
76
+ static int64_t gBgRuntimeGeneration = 0;
73
77
 
74
78
  using JavaObjectRef = std::shared_ptr<_jobject>;
75
79
 
@@ -108,7 +112,11 @@ static void leakAndClearRuntimeQueue(RuntimeWorkQueue &queue) {
108
112
  queue.scheduledDrainWorkId = -1;
109
113
  }
110
114
 
111
- static bool callScheduleOnJSThread(const JavaObjectRef &ref, bool isMain, int64_t workId) {
115
+ static bool callScheduleOnJSThread(
116
+ const JavaObjectRef &ref,
117
+ bool isMain,
118
+ int64_t workId,
119
+ int64_t runtimeGeneration) {
112
120
  JNIEnv *env = getJNIEnv();
113
121
  if (!env || !ref) {
114
122
  LOGE("executor: env=%p, ref=%p — aborting", env, ref.get());
@@ -125,7 +133,7 @@ static bool callScheduleOnJSThread(const JavaObjectRef &ref, bool isMain, int64_
125
133
  return false;
126
134
  }
127
135
 
128
- jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)Z");
136
+ jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJJ)Z");
129
137
  if (!mid) {
130
138
  LOGE("executor: scheduleOnJSThread method not found!");
131
139
  if (env->ExceptionCheck()) {
@@ -136,12 +144,14 @@ static bool callScheduleOnJSThread(const JavaObjectRef &ref, bool isMain, int64_
136
144
  return false;
137
145
  }
138
146
 
139
- LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", isMain, (long)workId);
147
+ LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld, generation=%ld)",
148
+ isMain, (long)workId, (long)runtimeGeneration);
140
149
  jboolean scheduled = env->CallBooleanMethod(
141
150
  ref.get(),
142
151
  mid,
143
152
  static_cast<jboolean>(isMain),
144
- static_cast<jlong>(workId));
153
+ static_cast<jlong>(workId),
154
+ static_cast<jlong>(runtimeGeneration));
145
155
  if (env->ExceptionCheck()) {
146
156
  LOGE("executor: JNI exception after scheduleOnJSThread");
147
157
  env->ExceptionDescribe();
@@ -155,15 +165,25 @@ static bool callScheduleOnJSThread(const JavaObjectRef &ref, bool isMain, int64_
155
165
 
156
166
  static void drainPendingBgEvals(const std::string &reason);
157
167
 
158
- static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain);
168
+ static void scheduleRuntimeDrain(
169
+ const JavaObjectRef &ref,
170
+ bool isMain,
171
+ int64_t runtimeGeneration);
159
172
 
160
- static void drainRuntimeWorkQueue(jsi::Runtime &rt, JavaObjectRef ref, bool isMain) {
173
+ static void drainRuntimeWorkQueue(
174
+ jsi::Runtime &rt,
175
+ JavaObjectRef ref,
176
+ bool isMain,
177
+ int64_t runtimeGeneration) {
161
178
  size_t drained = 0;
162
179
 
163
180
  while (drained < kRuntimeDrainBatchSize) {
164
181
  std::function<void(jsi::Runtime &)> work;
165
182
  {
166
183
  std::lock_guard<std::mutex> lock(gWorkMutex);
184
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
185
+ return;
186
+ }
167
187
  auto &queue = getRuntimeWorkQueue(isMain);
168
188
  if (queue.items.empty()) {
169
189
  break;
@@ -188,6 +208,9 @@ static void drainRuntimeWorkQueue(jsi::Runtime &rt, JavaObjectRef ref, bool isMa
188
208
  size_t remaining = 0;
189
209
  {
190
210
  std::lock_guard<std::mutex> lock(gWorkMutex);
211
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
212
+ return;
213
+ }
191
214
  auto &queue = getRuntimeWorkQueue(isMain);
192
215
  remaining = queue.items.size();
193
216
  if (remaining == 0) {
@@ -206,15 +229,21 @@ static void drainRuntimeWorkQueue(jsi::Runtime &rt, JavaObjectRef ref, bool isMa
206
229
  }
207
230
 
208
231
  if (shouldReschedule) {
209
- scheduleRuntimeDrain(ref, isMain);
232
+ scheduleRuntimeDrain(ref, isMain, runtimeGeneration);
210
233
  }
211
234
  }
212
235
 
213
- static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain) {
236
+ static void scheduleRuntimeDrain(
237
+ const JavaObjectRef &ref,
238
+ bool isMain,
239
+ int64_t runtimeGeneration) {
214
240
  int64_t workId;
215
241
  size_t queued = 0;
216
242
  {
217
243
  std::lock_guard<std::mutex> lock(gWorkMutex);
244
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
245
+ return;
246
+ }
218
247
  auto &queue = getRuntimeWorkQueue(isMain);
219
248
  // Stale-id guard: drainRuntimeWorkQueue observes remaining>0, drops the
220
249
  // lock, then calls us — but a concurrent nativeInvalidateSharedRpc can
@@ -233,17 +262,21 @@ static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain) {
233
262
  // Track the outstanding drain's workId so a teardown path can erase its
234
263
  // orphaned gPendingWork entry if the post is dropped during reload.
235
264
  queue.scheduledDrainWorkId = workId;
236
- gPendingWork[workId] = [ref, isMain](jsi::Runtime &rt) {
237
- drainRuntimeWorkQueue(rt, ref, isMain);
265
+ gPendingWork[workId] = [ref, isMain, runtimeGeneration](jsi::Runtime &rt) {
266
+ drainRuntimeWorkQueue(rt, ref, isMain, runtimeGeneration);
238
267
  };
239
268
  }
240
269
 
241
- bool scheduled = callScheduleOnJSThread(ref, isMain, workId);
270
+ bool scheduled = callScheduleOnJSThread(ref, isMain, workId, runtimeGeneration);
242
271
  if (!scheduled) {
272
+ bool shouldDrainBgEvals = false;
243
273
  {
244
274
  std::lock_guard<std::mutex> lock(gWorkMutex);
245
275
  gPendingWork.erase(workId);
246
- leakAndClearRuntimeQueue(getRuntimeWorkQueue(isMain));
276
+ if (isMain || runtimeGeneration == gBgRuntimeGeneration) {
277
+ leakAndClearRuntimeQueue(getRuntimeWorkQueue(isMain));
278
+ shouldDrainBgEvals = !isMain;
279
+ }
247
280
  LOGE("executor: failed to schedule runtime drain isMain=%d, workId=%ld, queued=%zu",
248
281
  isMain, (long)workId, queued);
249
282
  }
@@ -254,17 +287,25 @@ static void scheduleRuntimeDrain(const JavaObjectRef &ref, bool isMain) {
254
287
  // schedule hiccup must never falsely reject healthy bg evals. Called
255
288
  // outside gWorkMutex: drainPendingBgEvals takes gBgEvalMutex and does a
256
289
  // Java upcall, so it must not run under a native lock.
257
- if (!isMain) {
290
+ if (shouldDrainBgEvals) {
258
291
  drainPendingBgEvals("Background JS thread unreachable when scheduling segment eval");
259
292
  }
260
293
  }
261
294
  }
262
295
 
263
- static void enqueueRuntimeWork(JavaObjectRef ref, bool isMain, std::function<void(jsi::Runtime &)> work) {
296
+ static void enqueueRuntimeWork(
297
+ JavaObjectRef ref,
298
+ bool isMain,
299
+ int64_t runtimeGeneration,
300
+ std::function<void(jsi::Runtime &)> work) {
264
301
  bool shouldSchedule = false;
265
302
  size_t queued = 0;
266
303
  {
267
304
  std::lock_guard<std::mutex> lock(gWorkMutex);
305
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
306
+ new std::function<void(jsi::Runtime &)>(std::move(work));
307
+ return;
308
+ }
268
309
  auto &queue = getRuntimeWorkQueue(isMain);
269
310
  queue.items.push_back(std::move(work));
270
311
  queued = queue.items.size();
@@ -279,26 +320,39 @@ static void enqueueRuntimeWork(JavaObjectRef ref, bool isMain, std::function<voi
279
320
  }
280
321
 
281
322
  if (shouldSchedule) {
282
- scheduleRuntimeDrain(ref, isMain);
323
+ scheduleRuntimeDrain(ref, isMain, runtimeGeneration);
283
324
  }
284
325
  }
285
326
 
286
327
  // Called from Kotlin after runOnJSQueueThread dispatches to the correct thread.
287
328
  extern "C" JNIEXPORT void JNICALL
288
329
  Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
289
- JNIEnv *env, jobject thiz, jlong runtimePtr, jlong workId) {
290
- LOGI("nativeExecuteWork: runtimePtr=%ld, workId=%ld", (long)runtimePtr, (long)workId);
291
- auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
292
- if (!rt) return;
330
+ JNIEnv *env,
331
+ jobject thiz,
332
+ jlong runtimePtr,
333
+ jlong workId,
334
+ jboolean isMain,
335
+ jlong runtimeGeneration) {
336
+ LOGI("nativeExecuteWork: runtimePtr=%ld, workId=%ld, generation=%ld",
337
+ (long)runtimePtr, (long)workId, (long)runtimeGeneration);
293
338
 
294
339
  std::function<void(jsi::Runtime &)> work;
295
340
  {
296
341
  std::lock_guard<std::mutex> lock(gWorkMutex);
297
342
  auto it = gPendingWork.find(workId);
298
343
  if (it == gPendingWork.end()) return;
344
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
345
+ new std::function<void(jsi::Runtime &)>(std::move(it->second));
346
+ gPendingWork.erase(it);
347
+ LOGI("nativeExecuteWork: dropped stale background workId=%ld generation=%ld active=%ld",
348
+ (long)workId, (long)runtimeGeneration, (long)gBgRuntimeGeneration);
349
+ return;
350
+ }
299
351
  work = std::move(it->second);
300
352
  gPendingWork.erase(it);
301
353
  }
354
+ auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
355
+ if (!rt) return;
302
356
  try {
303
357
  work(*rt);
304
358
  } catch (const jsi::JSError &e) {
@@ -989,18 +1043,34 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeEvaluateSegmentInBackgro
989
1043
  // DOES run stale work (it can't — we erased it) would be a harmless no-op.
990
1044
  extern "C" JNIEXPORT void JNICALL
991
1045
  Java_com_backgroundthread_BackgroundThreadManager_nativeDropScheduledWork(
992
- JNIEnv * /* env */, jobject /* thiz */, jboolean isMain, jlong workId) {
1046
+ JNIEnv * /* env */,
1047
+ jobject /* thiz */,
1048
+ jboolean isMain,
1049
+ jlong workId,
1050
+ jlong runtimeGeneration) {
1051
+ bool shouldDrainBgEvals = false;
993
1052
  {
994
1053
  std::lock_guard<std::mutex> lock(gWorkMutex);
995
- gPendingWork.erase(static_cast<int64_t>(workId));
1054
+ auto pending = gPendingWork.find(static_cast<int64_t>(workId));
1055
+ if (!isMain && runtimeGeneration != gBgRuntimeGeneration) {
1056
+ if (pending != gPendingWork.end()) {
1057
+ new std::function<void(jsi::Runtime &)>(std::move(pending->second));
1058
+ gPendingWork.erase(pending);
1059
+ }
1060
+ return;
1061
+ }
1062
+ if (pending != gPendingWork.end()) {
1063
+ gPendingWork.erase(pending);
1064
+ }
996
1065
  // TRANSIENT ptr==0 (reload in flight): do NOT abandon queue.items — the
997
1066
  // recovered runtime still needs them (main has no JS retry net). Only
998
1067
  // disarm the drain latch so the next enqueue re-arms a fresh drain.
999
1068
  auto &queue = getRuntimeWorkQueue(static_cast<bool>(isMain));
1000
1069
  queue.drainScheduled = false;
1001
1070
  queue.scheduledDrainWorkId = -1;
1071
+ shouldDrainBgEvals = !isMain;
1002
1072
  }
1003
- if (!isMain) {
1073
+ if (shouldDrainBgEvals) {
1004
1074
  drainPendingBgEvals("Background runtime unreachable when scheduling segment eval");
1005
1075
  }
1006
1076
  }
@@ -1009,11 +1079,32 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeDropScheduledWork(
1009
1079
  // Install SharedStore and SharedRPC into a runtime.
1010
1080
  extern "C" JNIEXPORT void JNICALL
1011
1081
  Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
1012
- JNIEnv *env, jobject thiz, jlong runtimePtr, jboolean isMain) {
1082
+ JNIEnv *env,
1083
+ jobject thiz,
1084
+ jlong runtimePtr,
1085
+ jboolean isMain,
1086
+ jlong runtimeGeneration) {
1013
1087
 
1014
1088
  auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
1015
1089
  if (!rt) return;
1016
1090
 
1091
+ if (!isMain) {
1092
+ std::lock_guard<std::mutex> lock(gWorkMutex);
1093
+ if (runtimeGeneration < gBgRuntimeGeneration) {
1094
+ LOGI("nativeInstallSharedBridge: ignored stale background generation=%ld active=%ld",
1095
+ (long)runtimeGeneration, (long)gBgRuntimeGeneration);
1096
+ return;
1097
+ }
1098
+ if (runtimeGeneration != gBgRuntimeGeneration) {
1099
+ auto &queue = gBgRuntimeWorkQueue;
1100
+ if (queue.scheduledDrainWorkId >= 0) {
1101
+ gPendingWork.erase(queue.scheduledDrainWorkId);
1102
+ }
1103
+ leakAndClearRuntimeQueue(queue);
1104
+ gBgRuntimeGeneration = runtimeGeneration;
1105
+ }
1106
+ }
1107
+
1017
1108
  SharedStore::install(*rt);
1018
1109
 
1019
1110
  // Create executor that schedules work on this runtime's JS thread via Kotlin.
@@ -1027,13 +1118,19 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
1027
1118
  });
1028
1119
  bool capturedIsMain = static_cast<bool>(isMain);
1029
1120
 
1030
- RPCRuntimeExecutor executor = [ref, capturedIsMain](std::function<void(jsi::Runtime &)> work) {
1121
+ int64_t capturedRuntimeGeneration = static_cast<int64_t>(runtimeGeneration);
1122
+ RPCRuntimeExecutor executor = [ref, capturedIsMain, capturedRuntimeGeneration](
1123
+ std::function<void(jsi::Runtime &)> work) {
1031
1124
  // Coalesce per-runtime work into a single batched drain (see
1032
1125
  // enqueueRuntimeWork) instead of one Kotlin scheduleOnJSThread hop per
1033
1126
  // item. The bg-eval failure-drain that previously lived inline here now
1034
1127
  // runs in scheduleRuntimeDrain's schedule-failure path — under the
1035
1128
  // coalesced model that is the single place a schedule can fail.
1036
- enqueueRuntimeWork(ref, capturedIsMain, std::move(work));
1129
+ enqueueRuntimeWork(
1130
+ ref,
1131
+ capturedIsMain,
1132
+ capturedRuntimeGeneration,
1133
+ std::move(work));
1037
1134
  };
1038
1135
 
1039
1136
  std::string runtimeId = isMain ? "main" : "background";
@@ -1082,13 +1179,16 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
1082
1179
  {
1083
1180
  std::lock_guard<std::mutex> lock(gWorkMutex);
1084
1181
  auto &queue = getRuntimeWorkQueue(capturedIsMain);
1085
- if (!queue.items.empty()) {
1182
+ if (
1183
+ !queue.items.empty() &&
1184
+ (capturedIsMain || capturedRuntimeGeneration == gBgRuntimeGeneration)
1185
+ ) {
1086
1186
  queue.drainScheduled = true;
1087
1187
  shouldRecoverDrain = true;
1088
1188
  }
1089
1189
  }
1090
1190
  if (shouldRecoverDrain) {
1091
- scheduleRuntimeDrain(ref, capturedIsMain);
1191
+ scheduleRuntimeDrain(ref, capturedIsMain, capturedRuntimeGeneration);
1092
1192
  }
1093
1193
  }
1094
1194
 
@@ -1164,7 +1264,10 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeSetupErrorHandler(
1164
1264
  // instead of dispatching onto a dying runtime (parity with iOS).
1165
1265
  extern "C" JNIEXPORT jboolean JNICALL
1166
1266
  Java_com_backgroundthread_BackgroundThreadManager_nativeInvalidateSharedRpc(
1167
- JNIEnv *env, jobject thiz, jstring runtimeId) {
1267
+ JNIEnv *env,
1268
+ jobject thiz,
1269
+ jstring runtimeId,
1270
+ jlong runtimeGeneration) {
1168
1271
 
1169
1272
  const char *idChars = env->GetStringUTFChars(runtimeId, nullptr);
1170
1273
  if (!idChars) {
@@ -1173,8 +1276,6 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInvalidateSharedRpc(
1173
1276
  std::string id(idChars);
1174
1277
  env->ReleaseStringUTFChars(runtimeId, idChars);
1175
1278
 
1176
- bool found = SharedRPC::invalidate(id);
1177
-
1178
1279
  // The runtime named by `id` is being torn down (restart → host.reload).
1179
1280
  // Its coalesced work queue must be fully quiesced: if a drain was posted
1180
1281
  // (drainScheduled==true) but is dropped during reload, the latch would
@@ -1188,13 +1289,96 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInvalidateSharedRpc(
1188
1289
  std::lock_guard<std::mutex> lock(gWorkMutex);
1189
1290
  bool isMain = (id == "main");
1190
1291
  auto &queue = getRuntimeWorkQueue(isMain);
1292
+ if (!isMain) {
1293
+ gBgRuntimeGeneration = static_cast<int64_t>(runtimeGeneration);
1294
+ }
1191
1295
  if (queue.scheduledDrainWorkId >= 0) {
1192
1296
  gPendingWork.erase(queue.scheduledDrainWorkId);
1193
1297
  }
1194
1298
  leakAndClearRuntimeQueue(queue);
1195
1299
  }
1196
1300
 
1197
- LOGI("nativeInvalidateSharedRpc: id=%s found=%d", id.c_str(), found ? 1 : 0);
1301
+ bool found = SharedRPC::invalidate(id);
1302
+
1303
+ if (id == "background") {
1304
+ // Stop old timers from producing more work after the generation was
1305
+ // invalidated. Callback wrappers are intentionally leaked because
1306
+ // destroying a jsi::Function away from its outgoing JS thread is unsafe.
1307
+ std::lock_guard<std::mutex> lock(gTimerMutex);
1308
+ for (auto &entry : gTimers) {
1309
+ if (entry.second.callback) {
1310
+ new std::shared_ptr<jsi::Function>(std::move(entry.second.callback));
1311
+ }
1312
+ }
1313
+ gTimers.clear();
1314
+ gBgTimerExecutor = nullptr;
1315
+ gTimerCv.notify_all();
1316
+ }
1317
+
1318
+ if (id == "background") {
1319
+ drainPendingBgEvals("Background runtime invalidated before segment eval ran");
1320
+ }
1321
+
1322
+ LOGI("nativeInvalidateSharedRpc: id=%s generation=%ld found=%d",
1323
+ id.c_str(), (long)runtimeGeneration, found ? 1 : 0);
1324
+ return found ? JNI_TRUE : JNI_FALSE;
1325
+ }
1326
+
1327
+ // Invalidate the outgoing background runtime while executing on that runtime's
1328
+ // JS thread. This is the normal background-HMR path: it can reclaim timer and
1329
+ // queued-work callbacks instead of leaking their jsi::Function wrappers to
1330
+ // avoid an unsafe off-thread destructor.
1331
+ extern "C" JNIEXPORT jboolean JNICALL
1332
+ Java_com_backgroundthread_BackgroundThreadManager_nativeInvalidateBackgroundRuntimeOnJSThread(
1333
+ JNIEnv *env,
1334
+ jobject thiz,
1335
+ jlong runtimeGeneration) {
1336
+
1337
+ bool found = SharedRPC::invalidate("background");
1338
+
1339
+ // Prevent the timer worker from capturing or enqueueing another callback
1340
+ // while the outgoing JS thread drains the callback containers below.
1341
+ gTimerWorkerStop.store(true);
1342
+ gTimerCv.notify_all();
1343
+ if (gTimerWorkerThread.joinable()) {
1344
+ gTimerWorkerThread.join();
1345
+ }
1346
+ gTimerWorkerStarted.store(false);
1347
+
1348
+ std::vector<std::shared_ptr<jsi::Function>> timerCallbacks;
1349
+ {
1350
+ std::lock_guard<std::mutex> lock(gTimerMutex);
1351
+ timerCallbacks.reserve(gTimers.size());
1352
+ for (auto &entry : gTimers) {
1353
+ if (entry.second.callback) {
1354
+ timerCallbacks.push_back(std::move(entry.second.callback));
1355
+ }
1356
+ }
1357
+ gTimers.clear();
1358
+ gBgTimerExecutor = nullptr;
1359
+ }
1360
+
1361
+ std::deque<std::function<void(jsi::Runtime &)>> queuedWork;
1362
+ {
1363
+ std::lock_guard<std::mutex> lock(gWorkMutex);
1364
+ gBgRuntimeGeneration = static_cast<int64_t>(runtimeGeneration);
1365
+ auto &queue = gBgRuntimeWorkQueue;
1366
+ if (queue.scheduledDrainWorkId >= 0) {
1367
+ gPendingWork.erase(queue.scheduledDrainWorkId);
1368
+ }
1369
+ queuedWork.swap(queue.items);
1370
+ queue.drainScheduled = false;
1371
+ queue.scheduledDrainWorkId = -1;
1372
+ }
1373
+
1374
+ drainPendingBgEvals(
1375
+ "Background runtime invalidated before segment eval ran");
1376
+
1377
+ // timerCallbacks and queuedWork intentionally fall out of scope here, on
1378
+ // the outgoing runtime's JS thread, before ReactHost.destroy() begins.
1379
+ LOGI("nativeInvalidateBackgroundRuntimeOnJSThread: generation=%ld found=%d timers=%zu work=%zu",
1380
+ (long)runtimeGeneration, found ? 1 : 0,
1381
+ timerCallbacks.size(), queuedWork.size());
1198
1382
  return found ? JNI_TRUE : JNI_FALSE;
1199
1383
  }
1200
1384