fix: suspend-aware active-time watchdog for iOS split-bundle segments#65
Conversation
The 30s segment-eval watchdog used a bare dispatch_after; when the app was suspended during cold start while a segment's eval was buffered, the deadline elapsed during suspension and the stale watchdog won the SBLSettleGuard race on resume, false-rejecting the segment as SPLIT_BUNDLE_TIMEOUT (white screen). Replace it with SBLActiveWatchdog: a CLOCK_UPTIME_RAW active-time accumulator + cancelable dispatch_source timer that pauses on WillResignActive (sampling the resign instant synchronously so suspended time can't be folded in) and resumes with a 500ms grace on DidBecomeActive, giving the buffered executor a chance to flush before the watchdog can fire. Fails safe (fireLocked) if the timer source can't be created.
The active-time watchdog folded its pause on an async _queue block, so a timer tick already pending on _queue before WillResignActive could run first on resume — tickLocked recomputes `now` at execution time, so a suspension-delayed tick saw stale _isActive==YES + the old interval start and folded suspended-but-awake time, firing a false SPLIT_BUNDLE_TIMEOUT before the resume grace was armed. Fold + pause via dispatch_sync so the clock is stopped (and any pending tick is flushed at a valid pre-suspension instant) before the lifecycle callback returns. No deadlock: _queue blocks never sync back to main.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Problem
The 30s segment-eval watchdog in
ios/SplitBundleLoader.mmused a baredispatch_after(DISPATCH_TIME_NOW + 30s). When the app was backgrounded/suspended during cold start while a UI segment's eval was still buffered (waiting for the entry bundle to finish), the deadline elapsed during suspension. On foreground resume, the stale watchdog block won theSBLSettleGuardrace against the buffered runtime executor — which was ~1ms from succeeding — and false-rejected the segment asSPLIT_BUNDLE_TIMEOUT. The lazy route then hit its error boundary → white screen (matched in production logs: a ~10.5 min gap where the GCD timer only fired on resume).Fix
Replace the bare
dispatch_afterwithSBLActiveWatchdog:CLOCK_UPTIME_RAWactive-time accumulator — only foreground/active time counts toward the 30s, so suspended time never accrues.dispatch_source_tpolling timer (survives pause/resume, unlike a one-shotdispatch_after).UIApplicationWillResignActive(samples the resign instant synchronously so a delayed_queueblock can't fold suspended-but-awake time in), resumes with a 500ms grace onDidBecomeActiveso the buffered executor flushes first.fireLocked, retryable timeout) if the timer source can't be created — no indefinite hang.SBLSettleGuardremains the sole settle authority; success path cancels the watchdog; all mutable timing state serialized on a private serial queue.Review
Two adversarial review rounds (Claude + Codex). Round 2 caught the
handleWillResignActivesynchronous-timestamp bug (a residual instance of the same root cause) — fixed.Build / rollout
@onekeyfe/react-native-split-bundle-loader→ consume in app-monorepo (pod install) → ship with a native build. The JS-side amplifier fix (React.lazy self-heal) ships separately in app-monorepo and IS OTA-able.Test plan (on-device)
Cold launch → immediately background → wait > watchdog window → foreground. Before: white screen (false
SPLIT_BUNDLE_TIMEOUT). After: active-time excludes background, 500ms grace lets the executor flush, no false fire; a genuine entry wedge still times out correctly.