Skip to content

TurboModuleContext (default since 8.14) adds ~5 ms of CPU to every RNSentry call on iOS through native scope persistence #6886

Closed

Description

@arekm213

What React Native libraries do you use?

React Navigation, Hermes, RN New Architecture

Are you using sentry.io or on-premise?

sentry.io (SaS)

Are you using any other error monitoring solution alongside Sentry?

No

Other Error Monitoring Solution Name

No response

@sentry/react-native SDK Version

8.7.0 → 8.25.0 (measured)

How does your development environment look like?

System:
  OS: macOS 26.6.2
  CPU: (14) arm64 Apple M4 Pro
  Memory: 24.00 GB
Binaries:
  Node: 26.5.0
  npm: 11.17.0
Managers:
  CocoaPods: 1.16.2
npmPackages:
  "@react-native-community/cli": 20.1.0
  react: 19.2.3
  react-native: 0.86.0
  expo: 57.0.8
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true
Native SDKs (from @sentry/react-native 8.27.0): sentry-cocoa 9.29.0, sentry-android 8.57.0
Measurements below were taken on @sentry/react-native 8.25.0 (sentry-cocoa 9.26.1).

Sentry.init()

Sentry.init({
    dsn: CONFIG.SENTRY_DSN,
    tracesSampleRate: 1.0,
    enableAutoPerformanceTracing: true,
    enableUserInteractionTracing: true,
    integrations: [
        navigationIntegration, // Sentry.reactNavigationIntegration()
        tracingIntegration, // Sentry.reactNativeTracingIntegration()
        // + a few JS-only integrations (console, breadcrumbs, event filters)
    ],
    environment: CONFIG.ENVIRONMENT,
    release: `${pkg.name}@${semanticVersion}`,
    dist: buildNumber,
    enableLogs: true,
    // Native SDK is initialized early in AppDelegate / Application.onCreate
    autoInitializeNativeSdk: false,
    _experiments: {
        profilingOptions: {
            profileSessionSampleRate: 0.1,
            lifecycle: 'trace',
        },
    },
});

Full file: https://github.com/Expensify/App/blob/main/src/setup/telemetry/setupSentry.ts

Steps to Reproduce

  1. Use @sentry/react-native 8.14 or later with enableNative: true (default), so turboModuleContextIntegration() is added to the default integrations and wraps the RNSentry TurboModule.
  2. On iOS, call any wrapped RNSentry method in a loop, for example 300 sequential await RNSentry.fetchNativeFramesDelay(...) calls (any non-skipped method behaves the same), and measure the app process CPU time (ps -o time= -p before and after).
  3. Repeat with the integration filtered out:
    integrations: (defaults) => [...defaults.filter((i) => i.name !== 'TurboModuleContext'), ...ourIntegrations],
  4. For a real-world view, switch between bottom tabs 40 times (3 s apart) in an app with reactNavigationIntegration + tracing, and compare process CPU with and without the integration.

Expected Result

Crash-context instrumentation of the SDK's own RNSentry module costs roughly the same as the call it wraps, so the SDK's routine calls (fetchNativeFrames, fetchNativeFramesDelay, popTimeToDisplayFor, fetchNativeDeviceContexts, ...) stay cheap.

Actual Result

On iOS every wrapped RNSentry call becomes several native scope writes, and each native scope change is persisted to disk by the watchdog-termination scope observer.

Measured with a release bundle on the iOS simulator and a debug APK on the Android emulator, 2 runs per variant, app process CPU sampled every second:

                               iOS simulator            Android emulator
CPU per RNSentry call          ~4.7 ms vs ~0.03 ms      ~1.0 ms vs ~0.05 ms
40 tab switches, total CPU     28.7 / 29.7 s vs         44.6 / 43.8 s vs
                               19.6 / 17.6 s            41.4 / 41.7 s
per tab switch                 +0.27 s (+57%)           +0.07 s (+6.5%)
idle CPU                       no difference            no difference

(first value: integration on, the default; second: TurboModuleContext filtered out)

On a physical iPhone 17 Pro (release build, synthetic burst of back-to-back calls) the difference was ~19 ms vs ~0.2 ms of CPU per call.

A single bottom-tab switch makes about 54 RNSentry calls, all from the SDK itself (mostly fetchNativeFrames, popTimeToDisplayFor, fetchNativeFramesDelay, fetchNativeDeviceContexts, setActiveSpanId). We do not pass any modules to the integration.

Where the cost comes from (turboModuleTracker.js, 8.27.0):

  • Every wrapped call writes turbo_module context and two tags to the JS scope at start and again at end (syncToScope() / clearScope()). Each write is forwarded to the native scope via RNSentry.setContext / RNSentry.setTag, so one RNSentry call produces about 6 native scope writes.
  • On iOS, each native scope change is persisted by the watchdog-termination scope observer. The CPU time is spent on the io.sentry.watchdog-termination-tracking.fields-processor queue, writing the context/tags file. Idle CPU is unaffected.
  • ignoreTurboModules defaults to ['RNSentry'], so with default options the only output is the turbo_module tag on crash events naming the SDK method that was in flight.

Possible fixes:

  • Don't wrap RNSentry by default, or skip its frequently called read-only methods (fetchNativeFrames*, popTimeToDisplayFor, fetchNativeDeviceContexts).
  • Coalesce the native scope sync, or persist watchdog fields with debouncing on the cocoa side (similar to how breadcrumb persistence was changed in fix: Move breadcrumb file I/O off caller sentry-cocoa#8653).

Activity

  1. linear-code commented on Oct 13, 2026

    @linear-code
  2. alwx commented on Oct 14, 2026

    @alwx
    Contributor

    Thanks for reporting, @arekm213. We've seen your request and added it to our triage queue.

    We'll review it shortly and follow up if we need more information or once next steps are clearer.

  3. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Oct 14, 2026
  4. self-assigned this
    on Oct 16, 2026
  5. added a commit that references this issue on Oct 21, 2026
    3fa163e
  6. github-actions commented on Oct 22, 2026

    @github-actions
    Contributor

    A PR closing this issue has just been released 🚀

    This issue was referenced by PR #6921, which was included in the 8.32.0 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions