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)

Đang mở
#56,339 2 bình luận 1 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Needs: Attention Needs: Repro
Ngôn ngữ chính
C++
Star
127k
Fork
25.3k
Merge trung bình
1 ngày 23 giờ
Pull request đã merge (30 ngày)
4

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp tại ReactInstance::loadScript, sau đó đọc StartupLogger::logStartupEvent và reset() trong packages/react-native/ReactCommon/cxxreact/ReactMarker.cpp. Tái hiện việc reload JS bridgeless trên iOS và xác minh rằng các marker tương ứng runJSBundle và initReactRuntime vẫn khả dụng; sử dụng RNPerformanceManager.mm và lỗi performance.measure đã được báo cáo để xác nhận regression đã được khắc phục.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
cpp, ios, react-native
Lĩnh vực
mobile-dev
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
72/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.