diff --git a/CHANGELOG.md b/CHANGELOG.md index 237ba8308a..a9bbe7ed81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Add memory, CPU, and frame measurements to Android profiling ([#6250](https://github.com/getsentry/sentry-react-native/pull/6250)) - Add `enableAutoConsoleLogs` option to opt out of automatic `console.*` capture while keeping `enableLogs: true` for manual `Sentry.logger.*` calls ([#6235](https://github.com/getsentry/sentry-react-native/pull/6235)) - Warn when Gradle resolves `sentry-android` to a version incompatible with the SDK ([#6238](https://github.com/getsentry/sentry-react-native/pull/6238)) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 7fb53b8ef5..0e1053d42b 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -51,6 +51,8 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.profilemeasurements.ProfileMeasurement; +import io.sentry.profilemeasurements.ProfileMeasurementValue; import io.sentry.protocol.Geo; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; @@ -823,11 +825,23 @@ private void initializeAndroidProfiler() { } final String tracesFilesDirPath = getProfilingTracesDirPath(); + SentryFrameMetricsCollector collector = null; + try { + final SentryOptions options = Sentry.getCurrentScopes().getOptions(); + if (options instanceof SentryAndroidOptions) { + collector = ((SentryAndroidOptions) options).getFrameMetricsCollector(); + } + } catch (Throwable ignored) { // NOPMD - Best-effort + } + if (collector == null) { + collector = new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo); + } + androidProfiler.set( new AndroidProfiler( tracesFilesDirPath, (int) SECONDS.toMicros(1) / profilingTracesHz, - new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo), + collector, () -> executorService, logger)); } @@ -908,6 +922,27 @@ public WritableMap stopProfiling() { androidProfile.putString("sampled_profile", base64AndroidProfile); androidProfile.putInt("android_api_level", buildInfo.getSdkInfoVersion()); androidProfile.putString("build_id", getProguardUuid()); + + if (end.measurementsMap != null && !end.measurementsMap.isEmpty()) { + WritableMap measurements = new WritableNativeMap(); + for (Map.Entry entry : end.measurementsMap.entrySet()) { + WritableMap measurement = new WritableNativeMap(); + measurement.putString("unit", entry.getValue().getUnit()); + WritableArray values = new WritableNativeArray(); + if (entry.getValue().getValues() != null) { + for (ProfileMeasurementValue pmv : entry.getValue().getValues()) { + WritableMap value = new WritableNativeMap(); + value.putString("elapsed_since_start_ns", pmv.getRelativeStartNs()); + value.putDouble("value", pmv.getValue()); + values.pushMap(value); + } + } + measurement.putArray("values", values); + measurements.putMap(entry.getKey(), measurement); + } + androidProfile.putMap("measurements", measurements); + } + result.putMap("androidProfile", androidProfile); } } catch (Throwable e) { // NOPMD - We don't want to crash in any case diff --git a/packages/core/src/js/profiling/integration.ts b/packages/core/src/js/profiling/integration.ts index 2dc33e85a4..7f9692593a 100644 --- a/packages/core/src/js/profiling/integration.ts +++ b/packages/core/src/js/profiling/integration.ts @@ -305,12 +305,32 @@ export function createAndroidWithHermesProfile( nativeAndroid: NativeAndroidProfileEvent, durationNs: number, ): AndroidCombinedProfileEvent { + const { measurements: nativeMeasurements, ...rest } = nativeAndroid; + let measurements: AndroidCombinedProfileEvent['measurements']; + if (nativeMeasurements) { + measurements = {}; + for (const key of Object.keys(nativeMeasurements)) { + const nativeMeasurement = nativeMeasurements[key]; + if (!nativeMeasurement) { + continue; + } + measurements[key] = { + unit: nativeMeasurement.unit, + values: nativeMeasurement.values.map(v => ({ + elapsed_since_start_ns: Number(v.elapsed_since_start_ns), + value: v.value, + })), + }; + } + } + return { - ...nativeAndroid, + ...rest, platform: 'android', js_profile: hermes.profile, duration_ns: durationNs.toString(10), active_thread_id: hermes.transaction.active_thread_id, + ...(measurements && Object.keys(measurements).length > 0 && { measurements }), }; } diff --git a/packages/core/src/js/profiling/nativeTypes.ts b/packages/core/src/js/profiling/nativeTypes.ts index 583fd363ee..b61d5bfdcc 100644 --- a/packages/core/src/js/profiling/nativeTypes.ts +++ b/packages/core/src/js/profiling/nativeTypes.ts @@ -57,4 +57,14 @@ export interface NativeAndroidProfileEvent { * Proguard mapping file hash */ build_id?: string; + measurements?: Record< + string, + { + unit: string; + values: { + elapsed_since_start_ns: string; + value: number; + }[]; + } + >; } diff --git a/packages/core/src/js/profiling/types.ts b/packages/core/src/js/profiling/types.ts index 3c12ea6c87..143d5506e9 100644 --- a/packages/core/src/js/profiling/types.ts +++ b/packages/core/src/js/profiling/types.ts @@ -33,6 +33,7 @@ export type AndroidCombinedProfileEvent = { duration_ns: string; active_thread_id: string; profilingStartTimestampNs?: number; + measurements?: AndroidProfileEvent['measurements']; }; /* diff --git a/packages/core/test/profiling/fixtures.ts b/packages/core/test/profiling/fixtures.ts index f406a613f3..8f696c1ad5 100644 --- a/packages/core/test/profiling/fixtures.ts +++ b/packages/core/test/profiling/fixtures.ts @@ -166,3 +166,30 @@ export function createMockMinimalValidAndroidProfile(): NativeAndroidProfileEven build_id: 'mocked-build-id', }; } + +export function createMockMinimalValidAndroidProfileWithMeasurements(): NativeAndroidProfileEvent { + return { + ...createMockMinimalValidAndroidProfile(), + measurements: { + frozen_frame_renders: { + unit: 'nanosecond', + values: [{ elapsed_since_start_ns: '1000000', value: 800000000 }], + }, + slow_frame_renders: { + unit: 'nanosecond', + values: [{ elapsed_since_start_ns: '2000000', value: 20000000 }], + }, + cpu_usage: { + unit: 'percent', + values: [ + { elapsed_since_start_ns: '0', value: 35.5 }, + { elapsed_since_start_ns: '5000000', value: 42.1 }, + ], + }, + memory_footprint: { + unit: 'byte', + values: [{ elapsed_since_start_ns: '0', value: 104857600 }], + }, + }, + }; +} diff --git a/packages/core/test/profiling/integration.android.test.ts b/packages/core/test/profiling/integration.android.test.ts index eb5d324a60..6bf3c38aac 100644 --- a/packages/core/test/profiling/integration.android.test.ts +++ b/packages/core/test/profiling/integration.android.test.ts @@ -1,7 +1,11 @@ import type { AndroidCombinedProfileEvent } from '../../src/js/profiling/types'; import { createAndroidWithHermesProfile } from '../../src/js/profiling/integration'; -import { createMockMinimalValidAndroidProfile, createMockMinimalValidHermesProfileEvent } from './fixtures'; +import { + createMockMinimalValidAndroidProfile, + createMockMinimalValidAndroidProfileWithMeasurements, + createMockMinimalValidHermesProfileEvent, +} from './fixtures'; describe('merge Hermes and Android profiles - createAndroidWithHermesProfile', () => { it('should create Android profile structure with hermes profile', () => { @@ -49,4 +53,41 @@ describe('merge Hermes and Android profiles - createAndroidWithHermesProfile', ( active_thread_id: '123', }); }); + + it('should include measurements when present in native Android profile', () => { + const androidProfile = createMockMinimalValidAndroidProfileWithMeasurements(); + const result = createAndroidWithHermesProfile(createMockMinimalValidHermesProfileEvent(), androidProfile, 987); + + expect(result.measurements).toEqual({ + frozen_frame_renders: { + unit: 'nanosecond', + values: [{ elapsed_since_start_ns: 1000000, value: 800000000 }], + }, + slow_frame_renders: { + unit: 'nanosecond', + values: [{ elapsed_since_start_ns: 2000000, value: 20000000 }], + }, + cpu_usage: { + unit: 'percent', + values: [ + { elapsed_since_start_ns: 0, value: 35.5 }, + { elapsed_since_start_ns: 5000000, value: 42.1 }, + ], + }, + memory_footprint: { + unit: 'byte', + values: [{ elapsed_since_start_ns: 0, value: 104857600 }], + }, + }); + }); + + it('should not include measurements when absent from native Android profile', () => { + const result = createAndroidWithHermesProfile( + createMockMinimalValidHermesProfileEvent(), + createMockMinimalValidAndroidProfile(), + 987, + ); + + expect(result.measurements).toBeUndefined(); + }); }); diff --git a/packages/core/test/profiling/utils.test.ts b/packages/core/test/profiling/utils.test.ts index 1ab8630ca0..ab8d427e1f 100644 --- a/packages/core/test/profiling/utils.test.ts +++ b/packages/core/test/profiling/utils.test.ts @@ -147,4 +147,37 @@ describe('enrichAndroidProfileWithEventContext', () => { expect(result).not.toBeNull(); expect(result).not.toHaveProperty('profilingStartTimestampNs'); }); + + test('should include measurements when present', () => { + const measurements = { + cpu_usage: { + unit: 'percent' as const, + values: [ + { elapsed_since_start_ns: 0, value: 35.5 }, + { elapsed_since_start_ns: 5000000, value: 42.1 }, + ], + }, + memory_footprint: { + unit: 'byte' as const, + values: [{ elapsed_since_start_ns: 0, value: 104857600 }], + }, + }; + const profile = createMockAndroidCombinedProfile({ measurements }); + const event = createMockEvent(); + + const result = enrichAndroidProfileWithEventContext('profile-id', profile, event); + + expect(result).not.toBeNull(); + expect(result!.measurements).toEqual(measurements); + }); + + test('should not include measurements when absent', () => { + const profile = createMockAndroidCombinedProfile(); + const event = createMockEvent(); + + const result = enrichAndroidProfileWithEventContext('profile-id', profile, event); + + expect(result).not.toBeNull(); + expect(result!.measurements).toBeUndefined(); + }); });