@onekeyfe/react-native-split-bundle-loader 3.0.64 → 3.0.66
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/ios/SplitBundleLoader.mm +345 -20
- package/package.json +1 -1
package/ios/SplitBundleLoader.mm
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import <ReactCommon/RCTHost.h>
|
|
4
4
|
#import <ReactCommon/RCTHost+Internal.h>
|
|
5
5
|
#import <ReactCommon/RCTInstance.h>
|
|
6
|
+
#import <UIKit/UIKit.h>
|
|
6
7
|
#import <objc/runtime.h>
|
|
7
8
|
#import <CommonCrypto/CommonDigest.h>
|
|
8
9
|
#import <os/lock.h>
|
|
@@ -74,6 +75,313 @@ class NSDataJSIBuffer : public facebook::jsi::Buffer {
|
|
|
74
75
|
}
|
|
75
76
|
@end
|
|
76
77
|
|
|
78
|
+
// ACTIVE-TIME watchdog for the buffered runtime executor (replaces the bare
|
|
79
|
+
// dispatch_after C1 watchdog).
|
|
80
|
+
//
|
|
81
|
+
// WHY: the old watchdog was a single `dispatch_after(NOW + 30s)`. dispatch_after
|
|
82
|
+
// arms against an ABSOLUTE wall/uptime deadline that keeps elapsing while the
|
|
83
|
+
// app is backgrounded/suspended. If the app is suspended during cold start while
|
|
84
|
+
// a segment's eval is still BUFFERED (waiting for the entry bundle to finish),
|
|
85
|
+
// the 30s deadline can pass entirely off-screen; on FOREGROUND RESUME the stale
|
|
86
|
+
// block fires essentially instantly and wins the SBLSettleGuard race against the
|
|
87
|
+
// buffered executor that was ~1ms from succeeding — false-rejecting the segment
|
|
88
|
+
// as SPLIT_BUNDLE_TIMEOUT and white-screening the app.
|
|
89
|
+
//
|
|
90
|
+
// This watchdog instead accumulates ONLY foreground/active wall-time: suspended
|
|
91
|
+
// time never accrues toward the 30s, and a fresh foreground-grace window after
|
|
92
|
+
// every resume guarantees the buffered executor gets a chance to flush first, so
|
|
93
|
+
// a stale deadline can never fire instantly on resume.
|
|
94
|
+
//
|
|
95
|
+
// Time base: CLOCK_UPTIME_RAW already excludes device sleep; on top of that we
|
|
96
|
+
// only count intervals during which the app is in the active state. All mutable
|
|
97
|
+
// timing state is confined to a single serial queue (_queue) so the timer tick,
|
|
98
|
+
// the start/cancel calls, and the foreground/background notification callbacks
|
|
99
|
+
// never race. The fired/cancelled one-shot flag makes both onTimeout and teardown
|
|
100
|
+
// happen at most once regardless of which thread triggers them.
|
|
101
|
+
@interface SBLActiveWatchdog : NSObject
|
|
102
|
+
- (instancetype)initWithTimeoutMs:(uint64_t)timeoutMs
|
|
103
|
+
foregroundGraceMs:(uint64_t)foregroundGraceMs
|
|
104
|
+
onTimeout:(dispatch_block_t)onTimeout;
|
|
105
|
+
- (void)start;
|
|
106
|
+
- (void)cancel;
|
|
107
|
+
@end
|
|
108
|
+
|
|
109
|
+
@implementation SBLActiveWatchdog {
|
|
110
|
+
// Serial queue that owns ALL mutable state below — every read/write of these
|
|
111
|
+
// ivars happens on _queue, so no additional lock is needed.
|
|
112
|
+
dispatch_queue_t _queue;
|
|
113
|
+
dispatch_source_t _timer;
|
|
114
|
+
|
|
115
|
+
uint64_t _timeoutMs; // active-time budget before firing (e.g. 30000).
|
|
116
|
+
uint64_t _foregroundGraceMs; // post-resume window during which we won't fire.
|
|
117
|
+
|
|
118
|
+
uint64_t _accumulatedActiveMs; // folded active time from completed intervals.
|
|
119
|
+
uint64_t _currentIntervalStartUptimeNs; // CLOCK_UPTIME_RAW at active start.
|
|
120
|
+
uint64_t _graceUntilUptimeNs; // no fire before this uptime (grace window).
|
|
121
|
+
BOOL _isActive; // app currently in active/foreground state.
|
|
122
|
+
BOOL _finished; // one-shot: fired OR cancelled.
|
|
123
|
+
|
|
124
|
+
dispatch_block_t _onTimeout;
|
|
125
|
+
BOOL _observersRegistered;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
- (instancetype)initWithTimeoutMs:(uint64_t)timeoutMs
|
|
129
|
+
foregroundGraceMs:(uint64_t)foregroundGraceMs
|
|
130
|
+
onTimeout:(dispatch_block_t)onTimeout {
|
|
131
|
+
if (self = [super init]) {
|
|
132
|
+
_queue = dispatch_queue_create("com.onekey.splitbundle.watchdog", DISPATCH_QUEUE_SERIAL);
|
|
133
|
+
_timeoutMs = timeoutMs;
|
|
134
|
+
_foregroundGraceMs = foregroundGraceMs;
|
|
135
|
+
_onTimeout = [onTimeout copy];
|
|
136
|
+
_accumulatedActiveMs = 0;
|
|
137
|
+
_currentIntervalStartUptimeNs = 0;
|
|
138
|
+
_graceUntilUptimeNs = 0;
|
|
139
|
+
_isActive = NO;
|
|
140
|
+
_finished = NO;
|
|
141
|
+
_observersRegistered = NO;
|
|
142
|
+
}
|
|
143
|
+
return self;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static uint64_t SBLNowUptimeNs(void) {
|
|
147
|
+
return clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Current active-interval elapsed (ms) while _isActive; 0 otherwise. _queue only.
|
|
151
|
+
- (uint64_t)currentIntervalMsLocked {
|
|
152
|
+
if (!_isActive || _currentIntervalStartUptimeNs == 0) {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
uint64_t now = SBLNowUptimeNs();
|
|
156
|
+
if (now <= _currentIntervalStartUptimeNs) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
return (now - _currentIntervalStartUptimeNs) / 1000000ULL;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
- (void)start {
|
|
163
|
+
// Seed initial app-state + register observers on the MAIN thread:
|
|
164
|
+
// -applicationState and the notification center are both main-thread concerns.
|
|
165
|
+
// We then hop to _queue with the captured `activeNow` to prime timing + timer,
|
|
166
|
+
// so the timing-state ivars are only ever touched on _queue.
|
|
167
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
168
|
+
[self registerObservers];
|
|
169
|
+
// Treat anything other than Background as "active" for timing purposes
|
|
170
|
+
// (Inactive still makes progress toward the wedge).
|
|
171
|
+
UIApplicationState appState = UIApplication.sharedApplication.applicationState;
|
|
172
|
+
BOOL activeNow = (appState != UIApplicationStateBackground);
|
|
173
|
+
dispatch_async(self->_queue, ^{
|
|
174
|
+
[self primeTimerLockedWithActive:activeNow];
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Initialize timing state and create+resume the polling timer. _queue only.
|
|
180
|
+
- (void)primeTimerLockedWithActive:(BOOL)activeNow {
|
|
181
|
+
if (_finished) {
|
|
182
|
+
return; // cancelled before we got to prime — nothing to do.
|
|
183
|
+
}
|
|
184
|
+
_isActive = activeNow;
|
|
185
|
+
// Apply an initial grace window so a slow first foreground frame doesn't
|
|
186
|
+
// immediately trip the watchdog the very first tick. Read the clock ONCE so
|
|
187
|
+
// the interval start and the grace deadline share the same base instant.
|
|
188
|
+
if (activeNow) {
|
|
189
|
+
uint64_t now = SBLNowUptimeNs();
|
|
190
|
+
_currentIntervalStartUptimeNs = now;
|
|
191
|
+
_graceUntilUptimeNs = now + _foregroundGraceMs * 1000000ULL;
|
|
192
|
+
} else {
|
|
193
|
+
_currentIntervalStartUptimeNs = 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Polling timer: survives pause/resume trivially (unlike a one-shot
|
|
197
|
+
// dispatch_after). Tick every 500ms. Leeway gives the scheduler slack.
|
|
198
|
+
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
|
|
199
|
+
if (!_timer) {
|
|
200
|
+
// Fail safe: if the timer source can't be created we cannot guard against a
|
|
201
|
+
// wedge. Settle now (as a RETRYABLE timeout) via fireLocked rather than
|
|
202
|
+
// leaving the watchdog half-started (observers registered, _finished=NO,
|
|
203
|
+
// tick never firing) — that stuck state would hang loadSegment: forever.
|
|
204
|
+
// The JS loader re-attempts on SPLIT_BUNDLE_TIMEOUT, and the buffered
|
|
205
|
+
// executor, if it does run, still benefits the module table.
|
|
206
|
+
[self fireLocked];
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
dispatch_source_set_timer(_timer,
|
|
210
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)),
|
|
211
|
+
(uint64_t)(500 * NSEC_PER_MSEC),
|
|
212
|
+
(uint64_t)(100 * NSEC_PER_MSEC));
|
|
213
|
+
__weak SBLActiveWatchdog *weakSelf = self;
|
|
214
|
+
dispatch_source_set_event_handler(_timer, ^{
|
|
215
|
+
SBLActiveWatchdog *strongSelf = weakSelf;
|
|
216
|
+
if (strongSelf) {
|
|
217
|
+
[strongSelf tickLocked];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
dispatch_resume(_timer);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Timer tick — runs on _queue (the timer's own queue), so state is consistent.
|
|
224
|
+
- (void)tickLocked {
|
|
225
|
+
if (_finished) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!_isActive) {
|
|
229
|
+
return; // only foreground/active time counts toward the timeout.
|
|
230
|
+
}
|
|
231
|
+
uint64_t now = SBLNowUptimeNs();
|
|
232
|
+
if (now < _graceUntilUptimeNs) {
|
|
233
|
+
return; // inside the post-resume grace window — give the executor a chance.
|
|
234
|
+
}
|
|
235
|
+
uint64_t totalActiveMs = _accumulatedActiveMs + [self currentIntervalMsLocked];
|
|
236
|
+
if (totalActiveMs >= _timeoutMs) {
|
|
237
|
+
[self fireLocked];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fire onTimeout at most once, then tear everything down. _queue only.
|
|
242
|
+
- (void)fireLocked {
|
|
243
|
+
if (_finished) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
_finished = YES;
|
|
247
|
+
dispatch_block_t cb = _onTimeout;
|
|
248
|
+
_onTimeout = nil;
|
|
249
|
+
[self teardownTimerLocked];
|
|
250
|
+
// Remove observers on the main thread (where they were registered).
|
|
251
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
252
|
+
[self removeObservers];
|
|
253
|
+
});
|
|
254
|
+
if (cb) {
|
|
255
|
+
cb();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Cancel the timer source exactly once. _queue only.
|
|
260
|
+
- (void)teardownTimerLocked {
|
|
261
|
+
if (_timer) {
|
|
262
|
+
dispatch_source_cancel(_timer);
|
|
263
|
+
_timer = nil; // drop our ref; the source is retained until cancel completes.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Public cancel — thread-safe, may be called from the executor block's success
|
|
268
|
+
// path on a DIFFERENT thread. Hops onto _queue so it serializes with the tick.
|
|
269
|
+
- (void)cancel {
|
|
270
|
+
dispatch_async(_queue, ^{
|
|
271
|
+
if (self->_finished) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
self->_finished = YES;
|
|
275
|
+
self->_onTimeout = nil;
|
|
276
|
+
[self teardownTimerLocked];
|
|
277
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
278
|
+
[self removeObservers];
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// MARK: - App-state observers (main thread)
|
|
284
|
+
|
|
285
|
+
- (void)registerObservers {
|
|
286
|
+
if (_observersRegistered) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
_observersRegistered = YES;
|
|
290
|
+
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
291
|
+
[nc addObserver:self
|
|
292
|
+
selector:@selector(handleDidBecomeActive)
|
|
293
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
294
|
+
object:nil];
|
|
295
|
+
[nc addObserver:self
|
|
296
|
+
selector:@selector(handleWillResignActive)
|
|
297
|
+
name:UIApplicationWillResignActiveNotification
|
|
298
|
+
object:nil];
|
|
299
|
+
// DidEnterBackground is folded in too: on some transitions WillResignActive
|
|
300
|
+
// and DidEnterBackground both arrive; the resign handler is idempotent (it
|
|
301
|
+
// no-ops when already inactive), so treating background like resign is safe.
|
|
302
|
+
[nc addObserver:self
|
|
303
|
+
selector:@selector(handleWillResignActive)
|
|
304
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
305
|
+
object:nil];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
- (void)removeObservers {
|
|
309
|
+
if (!_observersRegistered) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
_observersRegistered = NO;
|
|
313
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
- (void)handleWillResignActive {
|
|
317
|
+
// Sample the resign instant on the main thread BEFORE the app can suspend.
|
|
318
|
+
uint64_t resignAtUptimeNs = SBLNowUptimeNs();
|
|
319
|
+
// Fold + pause SYNCHRONOUSLY (dispatch_sync, not async). The pause must take
|
|
320
|
+
// effect before this lifecycle callback returns — i.e. before the app is
|
|
321
|
+
// suspended. Why sync is required, not just a synchronous timestamp:
|
|
322
|
+
// - the polling timer fires on _queue; a tick may already be PENDING on
|
|
323
|
+
// _queue when we resign. tickLocked recomputes `now` at EXECUTION time, so
|
|
324
|
+
// if that pending tick doesn't run until the app RESUMES (minutes later),
|
|
325
|
+
// it would see _isActive==YES + the old interval start and fold all the
|
|
326
|
+
// suspended-but-awake time (CLOCK_UPTIME_RAW keeps advancing) → fire a
|
|
327
|
+
// false SPLIT_BUNDLE_TIMEOUT before the resume grace is even armed.
|
|
328
|
+
// - dispatch_sync drains the serial queue first: any pending tick runs NOW
|
|
329
|
+
// (at resign time, pre-suspension, with a valid small interval — no false
|
|
330
|
+
// fire), then this fold runs and sets _isActive=NO. So by the time the app
|
|
331
|
+
// suspends, the clock is stopped and no stale tick can fire on resume.
|
|
332
|
+
// Safe from deadlock: _queue blocks never dispatch_sync back to the main
|
|
333
|
+
// thread (observer removal uses dispatch_async), and the queue's blocks are
|
|
334
|
+
// all O(1).
|
|
335
|
+
dispatch_sync(_queue, ^{
|
|
336
|
+
if (self->_finished || !self->_isActive) {
|
|
337
|
+
return; // idempotent: already inactive or already settled.
|
|
338
|
+
}
|
|
339
|
+
// Fold ONLY the active interval up to resignAt into the accumulator, then
|
|
340
|
+
// stop the clock. Suspended time after this point does NOT accrue.
|
|
341
|
+
if (self->_currentIntervalStartUptimeNs != 0 &&
|
|
342
|
+
resignAtUptimeNs > self->_currentIntervalStartUptimeNs) {
|
|
343
|
+
self->_accumulatedActiveMs +=
|
|
344
|
+
(resignAtUptimeNs - self->_currentIntervalStartUptimeNs) / 1000000ULL;
|
|
345
|
+
}
|
|
346
|
+
self->_isActive = NO;
|
|
347
|
+
self->_currentIntervalStartUptimeNs = 0;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
- (void)handleDidBecomeActive {
|
|
352
|
+
dispatch_async(_queue, ^{
|
|
353
|
+
if (self->_finished || self->_isActive) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Resume the clock and arm a fresh grace window so the buffered executor
|
|
357
|
+
// gets a chance to flush before the watchdog can fire again. One clock read
|
|
358
|
+
// so the interval start and the grace deadline share the same base instant.
|
|
359
|
+
uint64_t now = SBLNowUptimeNs();
|
|
360
|
+
self->_isActive = YES;
|
|
361
|
+
self->_currentIntervalStartUptimeNs = now;
|
|
362
|
+
self->_graceUntilUptimeNs = now + self->_foregroundGraceMs * 1000000ULL;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
- (void)dealloc {
|
|
367
|
+
// Defensive: observers are normally removed via cancel/fire, but if this
|
|
368
|
+
// object is released without either (shouldn't happen — the executor block
|
|
369
|
+
// retains it until cancel), make sure we don't leave a dangling observer.
|
|
370
|
+
if (_observersRegistered) {
|
|
371
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
372
|
+
}
|
|
373
|
+
// Make the "timer is nil by dealloc" invariant explicit: cancel/fireLocked
|
|
374
|
+
// both null _timer before the last retain drops, but guard here so a future
|
|
375
|
+
// early-return on those paths can't orphan a running dispatch_source_t. Safe
|
|
376
|
+
// to touch _timer without hopping to _queue — no other thread references a
|
|
377
|
+
// deallocating object.
|
|
378
|
+
if (_timer) {
|
|
379
|
+
dispatch_source_cancel(_timer);
|
|
380
|
+
_timer = nil;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
@end
|
|
384
|
+
|
|
77
385
|
// Watchdog window for the buffered runtime executor (C1). Post-entry eval is
|
|
78
386
|
// sub-millisecond, so this only ever elapses on a genuine wedge (entry bundle
|
|
79
387
|
// never finished evaluating) — never on a healthy slow device.
|
|
@@ -362,11 +670,30 @@ typedef NS_ENUM(NSInteger, ESegmentEvalError) {
|
|
|
362
670
|
// either order, on a wedge).
|
|
363
671
|
SBLSettleGuard *settleGuard = [[SBLSettleGuard alloc] init];
|
|
364
672
|
|
|
673
|
+
// C1 active-time watchdog. Constructed BEFORE the executor block so the block
|
|
674
|
+
// can capture (retain) it and tear it down on the happy path. It fires only
|
|
675
|
+
// on a genuine wedge AND only after kSegmentEvalWatchdogSeconds of
|
|
676
|
+
// FOREGROUND/active time has accrued — backgrounded/suspended time never
|
|
677
|
+
// counts, so a stale deadline can never fire instantly on a foreground
|
|
678
|
+
// resume (the white-screen bug this replaces). On fire it settles the SAME
|
|
679
|
+
// SBLSettleGuard via tryClaim, so there is still exactly one settle.
|
|
680
|
+
SBLActiveWatchdog *watchdog = [[SBLActiveWatchdog alloc]
|
|
681
|
+
initWithTimeoutMs:(uint64_t)(kSegmentEvalWatchdogSeconds * 1000.0)
|
|
682
|
+
foregroundGraceMs:500
|
|
683
|
+
onTimeout:^{
|
|
684
|
+
if ([settleGuard tryClaim]) {
|
|
685
|
+
[SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ (key=%@) WATCHDOG fired after %.0fs active time — runtime executor never ran (entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, segmentKey, kSegmentEvalWatchdogSeconds]];
|
|
686
|
+
onEvaluated([NSError errorWithDomain:@"SplitBundleLoader"
|
|
687
|
+
code:ESegmentEvalErrorTimeout
|
|
688
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment %@ eval timed out after %.0fs active time (buffered runtime executor never ran)", segmentKey, kSegmentEvalWatchdogSeconds]}]);
|
|
689
|
+
}
|
|
690
|
+
}];
|
|
691
|
+
|
|
365
692
|
[instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) {
|
|
366
693
|
@autoreleasepool {
|
|
367
|
-
// If the watchdog already fired (entry took >30s then
|
|
368
|
-
// the JS promise is already rejected — still evaluate the
|
|
369
|
-
// (the module table benefits) but don't double-settle.
|
|
694
|
+
// If the watchdog already fired (entry took >30s active then
|
|
695
|
+
// unwedged), the JS promise is already rejected — still evaluate the
|
|
696
|
+
// segment (the module table benefits) but don't double-settle.
|
|
370
697
|
BOOL won = [settleGuard tryClaim];
|
|
371
698
|
NSError *evalError = nil;
|
|
372
699
|
CFAbsoluteTime evalStart = CFAbsoluteTimeGetCurrent();
|
|
@@ -389,33 +716,31 @@ typedef NS_ENUM(NSInteger, ESegmentEvalError) {
|
|
|
389
716
|
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment evaluation failed for %@ (unknown C++ exception)", sourceURL]}];
|
|
390
717
|
[SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluation threw an unknown exception", sourceURL]];
|
|
391
718
|
}
|
|
719
|
+
// Tear down the active-time watchdog (timer + observers) regardless
|
|
720
|
+
// of who won the settle: the buffered executor has now run, so the
|
|
721
|
+
// watchdog has no further job. Capturing `watchdog` here is also what
|
|
722
|
+
// RETAINS it for the lifetime of this async block (until cancel). It
|
|
723
|
+
// is safe to cancel from this (different) thread — cancel hops onto
|
|
724
|
+
// the watchdog's own serial queue and is a one-shot.
|
|
725
|
+
[watchdog cancel];
|
|
392
726
|
if (won) {
|
|
393
727
|
// Resolve/reject the JS promise from INSIDE this same block,
|
|
394
728
|
// strictly AFTER the segment eval above — the ordering guarantee
|
|
395
729
|
// that fixes the "Requiring unknown module" race (see method doc).
|
|
396
730
|
onEvaluated(evalError);
|
|
397
731
|
} else {
|
|
398
|
-
[SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated AFTER watchdog already settled (entry was wedged >%.0fs)", sourceURL, kSegmentEvalWatchdogSeconds]];
|
|
732
|
+
[SBLLogger warn:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ evaluated AFTER watchdog already settled (entry was wedged >%.0fs active time)", sourceURL, kSegmentEvalWatchdogSeconds]];
|
|
399
733
|
}
|
|
400
734
|
}
|
|
401
735
|
}];
|
|
402
736
|
|
|
403
|
-
//
|
|
404
|
-
// bundle is long done and the buffered block
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kSegmentEvalWatchdogSeconds * NSEC_PER_SEC)),
|
|
411
|
-
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
412
|
-
if ([settleGuard tryClaim]) {
|
|
413
|
-
[SBLLogger error:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ (key=%@) WATCHDOG fired after %.0fs — runtime executor never ran (entry bundle likely never finished evaluating). Rejecting as retryable timeout.", sourceURL, segmentKey, kSegmentEvalWatchdogSeconds]];
|
|
414
|
-
onEvaluated([NSError errorWithDomain:@"SplitBundleLoader"
|
|
415
|
-
code:ESegmentEvalErrorTimeout
|
|
416
|
-
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Segment %@ eval timed out after %.0fs (buffered runtime executor never ran)", segmentKey, kSegmentEvalWatchdogSeconds]}]);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
737
|
+
// Arm the watchdog AFTER scheduling the executor. Fires only on a genuine
|
|
738
|
+
// wedge: in steady state the entry bundle is long done and the buffered block
|
|
739
|
+
// runs sub-millisecond, so it cancels the watchdog (and the guard is already
|
|
740
|
+
// claimed) long before kSegmentEvalWatchdogSeconds of ACTIVE time elapses.
|
|
741
|
+
// Because only foreground/active time is counted, a suspend-during-cold-start
|
|
742
|
+
// can no longer let a stale deadline false-fire on resume.
|
|
743
|
+
[watchdog start];
|
|
419
744
|
|
|
420
745
|
double dispatchMs = (CFAbsoluteTimeGetCurrent() - dispatchStart) * 1000.0;
|
|
421
746
|
[SBLLogger info:[NSString stringWithFormat:@"[SplitBundle] loadSegment: %@ dispatched in %.1fms (resolve fires after eval; watchdog %.0fs)", sourceURL, dispatchMs, kSegmentEvalWatchdogSeconds]];
|