You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
runJsBundleStart performance marker dropped on JS reload due to APP_STARTUP_START reset cascade in ReactInstance::loadScript (regression in 0.83) #56339
After commit 796d182 (PR #54255 "Add missing INIT_REACT_RUNTIME_START and APP_STARTUP_START in loadScript"), ReactInstance::loadScript triggers an unintended StartupLogger::reset() cascade on every JS reload. This causes runJSBundleStartTime (and initReactRuntimeStartTime) to be NaN-cleared after their NaN guards have already rejected the new value, while the corresponding STOP markers fire after the reset and successfully populate fresh values.
The asymmetric outcome is that on every JS reload (press R, dev menu reload, fast refresh), runJSBundleStartTime ends as NaN while runJSBundleEndTime ends as a valid timestamp. Any consumer that pairs them — notably react-native-performance@5.1.4's iOS native module — silently drops the runJsBundleStart mark (its int64_t cast of NaN becomes 0, which hits the if (mediaTime == 0) return; guard in RNPerformanceManager.mm::emitMarkNamed:withMediaTime:).
For applications calling performance.measure('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd'), this surfaces as:
Failed to execute 'measure' on 'Performance': The mark 'runJsBundleStart' does not exist.
The bug only manifests on JS reload, not cold launch. Production cold-start telemetry is unaffected, so most teams won't notice — but it breaks dev iteration with a LogBox error on every refresh, and breaks any second-instance metrics for libraries that rely on StartupLogger.
Root cause walkthrough
StartupLogger::logStartupEvent in packages/react-native/ReactCommon/cxxreact/ReactMarker.cpp has a hidden side-effect on APP_STARTUP_START: if appStartupStartTime is already set, it calls reset() which NaN-clears all six startup fields. This was originally designed for warm-restart scenarios (the process survives but the app reopens), not for being triggered inside loadScript.
After PR #54255, ReactInstance::loadScript now calls APP_STARTUP_START from inside the bridgeless reload path. On a JS-only reload, the resulting sequence is:
appStartupStartTime non-NaN → reset() NaN-clears all six fields, then sets appStartupStartTime = now
4
runtime.evaluateJavaScript(...)
(bundle executes)
5
RUN_JS_BUNDLE_STOP
runJSBundleEndTime is now NaN (cleared in step 3) → populated with fresh time ✅
6
INIT_REACT_RUNTIME_STOP
NaN → populated ✅
7
APP_STARTUP_STOP
NaN → populated ✅
Final state on reload:
runJSBundleStartTime = NaN ← LOST (rejected by guard in step 1, then wiped in step 3)
runJSBundleEndTime = valid (new)
initReactRuntimeStart = NaN ← LOST (same reason)
initReactRuntimeEnd = valid (new)
appStartupStartTime = valid (new)
appStartupEndTime = valid (new)
Asymmetric outcome: STARTs get wiped after their guard rejects them; STOPs get wiped before their guard accepts them.
Steps to reproduce
Create or upgrade an app to React Native ≥0.83.0 with the new architecture enabled.
Subscribe to the runJsBundleStart and runJsBundleEnd marks and call performance.measure(...) between them when runJsBundleEnd arrives. Minimal example:
Build and launch on iOS — the metric is captured correctly. ✅
Press R in the dev menu to reload JS — the LogBox shows Failed to execute 'measure' on 'Performance': The mark 'runJsBundleStart' does not exist. ❌
You can also confirm the silent drop by checking Xcode console for Ignoring mark named runJsBundleStart as timestamp is not set from RNPerformanceManager.mm.
Happy to provide a minimal reproducer repo if needed.
React Native Version
0.83.4 (also reproduced in 0.84.1; same code in 0.85.0-rc.7 and main)
Affected Platforms
Runtime - iOS (new architecture / bridgeless)
Affected versions
✅ 0.81.x: not affected (only RUN_JS_BUNDLE_START was called in loadScript; the reset cascade was never triggered)
❌ 0.83.0 - 0.83.4
❌ 0.84.0 - 0.84.1
❌ 0.85.0-rc.7
❌ main (0.86 nightly)
Suggested fix
Call StartupLogger::reset() explicitly at the top of the lambda in ReactInstance::loadScript, before the if (hasLogger) block. This makes the reset intent explicit rather than relying on the APP_STARTUP_START handler's side-effect, and ensures every loadScript call (including reloads) starts with a clean StartupLogger:
A cleaner alternative would be to remove the auto-reset side effect from the APP_STARTUP_START handler in StartupLogger::logStartupEvent and require callers to invoke reset() explicitly.
Workaround for affected apps
Until this is fixed upstream, apps can defensively guard the measure() call:
if(performance.getEntriesByName('runJsBundleStart').length===0){return;// skip metric on dev refresh}performance.measure('runJsBundle','runJsBundleStart','runJsBundleEnd');
Or apply the upstream fix as a local patch via patch-package / pnpm patch until a release contains it.
Description
After commit 796d182 (PR #54255 "Add missing INIT_REACT_RUNTIME_START and APP_STARTUP_START in loadScript"),
ReactInstance::loadScripttriggers an unintendedStartupLogger::reset()cascade on every JS reload. This causesrunJSBundleStartTime(andinitReactRuntimeStartTime) to be NaN-cleared after their NaN guards have already rejected the new value, while the correspondingSTOPmarkers fire after the reset and successfully populate fresh values.The asymmetric outcome is that on every JS reload (press R, dev menu reload, fast refresh),
runJSBundleStartTimeends as NaN whilerunJSBundleEndTimeends as a valid timestamp. Any consumer that pairs them — notablyreact-native-performance@5.1.4's iOS native module — silently drops therunJsBundleStartmark (itsint64_tcast of NaN becomes 0, which hits theif (mediaTime == 0) return;guard inRNPerformanceManager.mm::emitMarkNamed:withMediaTime:).For applications calling
performance.measure('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd'), this surfaces as:The bug only manifests on JS reload, not cold launch. Production cold-start telemetry is unaffected, so most teams won't notice — but it breaks dev iteration with a LogBox error on every refresh, and breaks any second-instance metrics for libraries that rely on
StartupLogger.Root cause walkthrough
StartupLogger::logStartupEventinpackages/react-native/ReactCommon/cxxreact/ReactMarker.cpphas a hidden side-effect onAPP_STARTUP_START: ifappStartupStartTimeis already set, it callsreset()which NaN-clears all six startup fields. This was originally designed for warm-restart scenarios (the process survives but the app reopens), not for being triggered insideloadScript.After PR #54255,
ReactInstance::loadScriptnow callsAPP_STARTUP_STARTfrom inside the bridgeless reload path. On a JS-only reload, the resulting sequence is:RUN_JS_BUNDLE_STARTrunJSBundleStartTimealready non-NaN → no-op (still cold-start value)INIT_REACT_RUNTIME_STARTinitReactRuntimeStartTimealready non-NaN → no-opAPP_STARTUP_STARTappStartupStartTimenon-NaN →reset()NaN-clears all six fields, then setsappStartupStartTime = nowruntime.evaluateJavaScript(...)RUN_JS_BUNDLE_STOPrunJSBundleEndTimeis now NaN (cleared in step 3) → populated with fresh time ✅INIT_REACT_RUNTIME_STOPAPP_STARTUP_STOPFinal state on reload:
Asymmetric outcome: STARTs get wiped after their guard rejects them; STOPs get wiped before their guard accepts them.
Steps to reproduce
react-native-performance@5.1.4(or any version since Adds bridging header helper to facilitate importing to swift #115 added the StartupLogger reads).runJsBundleStartandrunJsBundleEndmarks and callperformance.measure(...)between them whenrunJsBundleEndarrives. Minimal example:Failed to execute 'measure' on 'Performance': The mark 'runJsBundleStart' does not exist.❌You can also confirm the silent drop by checking Xcode console for
Ignoring mark named runJsBundleStart as timestamp is not setfromRNPerformanceManager.mm.Happy to provide a minimal reproducer repo if needed.
React Native Version
0.83.4 (also reproduced in 0.84.1; same code in 0.85.0-rc.7 and main)
Affected Platforms
Runtime - iOS (new architecture / bridgeless)
Affected versions
RUN_JS_BUNDLE_STARTwas called inloadScript; the reset cascade was never triggered)Suggested fix
Call
StartupLogger::reset()explicitly at the top of the lambda inReactInstance::loadScript, before theif (hasLogger)block. This makes the reset intent explicit rather than relying on theAPP_STARTUP_STARThandler's side-effect, and ensures everyloadScriptcall (including reloads) starts with a clean StartupLogger:A cleaner alternative would be to remove the auto-reset side effect from the
APP_STARTUP_STARThandler inStartupLogger::logStartupEventand require callers to invokereset()explicitly.Workaround for affected apps
Until this is fixed upstream, apps can defensively guard the
measure()call:Or apply the upstream fix as a local patch via
patch-package/pnpm patchuntil a release contains it.Related
react-native-performancePR that added the bridgelessStartupLoggerreads: Fix bridgeless mode on iOS oblador/react-native-performance#115