react / react/react-native

runJsBundleStart performance marker dropped on JS reload due to APP_STARTUP_START reset cascade in ReactInstance::loadScript (regression in 0.83)

Abierto
#56,339 2 comentarios 1 reacción 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Needs: Attention Needs: Repro
Lenguaje dominante
C++
Estrellas
127k
Forks
25.3k
Merge medio
1 d 23 h
PR fusionados (30 d)
4

Descripción

Description

After commit 796d182d8989ff826eaa3a57458bdfc79750f820 (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:

// In ReactInstance::loadScript (current main, lines 240-246)
if (hasLogger) {
  ReactMarker::logTaggedMarkerBridgeless(
      ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str());        // step 1
  ReactMarker::logMarkerBridgeless(ReactMarker::INIT_REACT_RUNTIME_START); // step 2
  ReactMarker::logMarkerBridgeless(ReactMarker::APP_STARTUP_START); // step 3
}
Step Call Effect on StartupLogger (on second loadScript invocation)
1 RUN_JS_BUNDLE_START runJSBundleStartTime already non-NaN → no-op (still cold-start value)
2 INIT_REACT_RUNTIME_START initReactRuntimeStartTime already non-NaN → no-op
3 APP_STARTUP_START 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
  1. Create or upgrade an app to React Native ≥0.83.0 with the new architecture enabled.
  2. Install react-native-performance@5.1.4 (or any version since #115 added the StartupLogger reads).
  3. Subscribe to the runJsBundleStart and runJsBundleEnd marks and call performance.measure(...) between them when runJsBundleEnd arrives. Minimal example:
import { useEffect } from 'react';
import performance, { PerformanceObserver } from 'react-native-performance';

useEffect(() => {
  new PerformanceObserver((list) => {
    if (list.getEntries().some((e) => e.name === 'runJsBundleEnd')) {
      performance.measure('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
    }
  }).observe({ type: 'react-native-mark', buffered: true });
}, []);
  1. Build and launch on iOS — the metric is captured correctly. ✅
  2. 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:

diff --git a/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp b/packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp
@@ -237,6 +237,7 @@ void ReactInstance::loadScript(
     if (beforeLoad) {
       beforeLoad(runtime);
     }
     TraceSection s("ReactInstance::loadScript");
+    ReactMarker::StartupLogger::getInstance().reset();
     bool hasLogger(ReactMarker::logTaggedMarkerBridgelessImpl != nullptr);
     if (hasLogger) {
       ReactMarker::logTaggedMarkerBridgeless(
           ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str());

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.

Related
  • PR that introduced the regression: #54255
  • Issue that motivated #54255: #53818
  • react-native-performance PR that added the bridgeless StartupLogger reads: oblador/react-native-performance#115

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza en packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp, en ReactInstance::loadScript, y después lee StartupLogger::logStartupEvent y reset() en packages/react-native/ReactCommon/cxxreact/ReactMarker.cpp. Reproduce una recarga de JS de iOS bridgeless y verifica que los marcadores emparejados runJSBundle e initReactRuntime sigan disponibles; usa RNPerformanceManager.mm y el fallo reportado de performance.measure para confirmar que la regresión se ha resuelto.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
cpp, ios, react-native
Área
mobile-dev
Tipo de issue
Error
Dificultad
3/5
Tiempo estimado
1-2 días
Estado de actividad
Tranquilo
Claridad
Bien especificado
Aptitud para principiantes
72/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.