Skip to content

Commit cdb2ab0

Browse files
antonisclaude
andauthored
feat(profiling): Add measurements to Android profiling (#6250)
* feat(profiling): Add measurements to Android profiling Pass memory, CPU, and frame measurements from the Android SDK's ProfileEndData through the React Native bridge into profile events. The data was already collected by the native profiler but never forwarded to JS. Closes #3641 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: Update changelog PR link for #6250 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(android): Reuse SDK frame metrics collector for profiling The standalone SentryFrameMetricsCollector created in initializeAndroidProfiler() missed the onActivityStarted callback because the activity was already started, so currentWindow was never set and no frame metrics were collected. Reuse the collector from SentryAndroidOptions instead, which is already tracking the active window from Sentry.init(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7ff4d0f commit cdb2ab0

8 files changed

Lines changed: 171 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Features
1212

13+
- Add memory, CPU, and frame measurements to Android profiling ([#6250](https://github.com/getsentry/sentry-react-native/pull/6250))
1314
- 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))
1415
- Warn when Gradle resolves `sentry-android` to a version incompatible with the SDK ([#6238](https://github.com/getsentry/sentry-react-native/pull/6238))
1516

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
5252
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
5353
import io.sentry.android.core.performance.AppStartMetrics;
54+
import io.sentry.profilemeasurements.ProfileMeasurement;
55+
import io.sentry.profilemeasurements.ProfileMeasurementValue;
5456
import io.sentry.protocol.Geo;
5557
import io.sentry.protocol.SdkVersion;
5658
import io.sentry.protocol.SentryId;
@@ -823,11 +825,23 @@ private void initializeAndroidProfiler() {
823825
}
824826
final String tracesFilesDirPath = getProfilingTracesDirPath();
825827

828+
SentryFrameMetricsCollector collector = null;
829+
try {
830+
final SentryOptions options = Sentry.getCurrentScopes().getOptions();
831+
if (options instanceof SentryAndroidOptions) {
832+
collector = ((SentryAndroidOptions) options).getFrameMetricsCollector();
833+
}
834+
} catch (Throwable ignored) { // NOPMD - Best-effort
835+
}
836+
if (collector == null) {
837+
collector = new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo);
838+
}
839+
826840
androidProfiler.set(
827841
new AndroidProfiler(
828842
tracesFilesDirPath,
829843
(int) SECONDS.toMicros(1) / profilingTracesHz,
830-
new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo),
844+
collector,
831845
() -> executorService,
832846
logger));
833847
}
@@ -908,6 +922,27 @@ public WritableMap stopProfiling() {
908922
androidProfile.putString("sampled_profile", base64AndroidProfile);
909923
androidProfile.putInt("android_api_level", buildInfo.getSdkInfoVersion());
910924
androidProfile.putString("build_id", getProguardUuid());
925+
926+
if (end.measurementsMap != null && !end.measurementsMap.isEmpty()) {
927+
WritableMap measurements = new WritableNativeMap();
928+
for (Map.Entry<String, ProfileMeasurement> entry : end.measurementsMap.entrySet()) {
929+
WritableMap measurement = new WritableNativeMap();
930+
measurement.putString("unit", entry.getValue().getUnit());
931+
WritableArray values = new WritableNativeArray();
932+
if (entry.getValue().getValues() != null) {
933+
for (ProfileMeasurementValue pmv : entry.getValue().getValues()) {
934+
WritableMap value = new WritableNativeMap();
935+
value.putString("elapsed_since_start_ns", pmv.getRelativeStartNs());
936+
value.putDouble("value", pmv.getValue());
937+
values.pushMap(value);
938+
}
939+
}
940+
measurement.putArray("values", values);
941+
measurements.putMap(entry.getKey(), measurement);
942+
}
943+
androidProfile.putMap("measurements", measurements);
944+
}
945+
911946
result.putMap("androidProfile", androidProfile);
912947
}
913948
} catch (Throwable e) { // NOPMD - We don't want to crash in any case

packages/core/src/js/profiling/integration.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,32 @@ export function createAndroidWithHermesProfile(
305305
nativeAndroid: NativeAndroidProfileEvent,
306306
durationNs: number,
307307
): AndroidCombinedProfileEvent {
308+
const { measurements: nativeMeasurements, ...rest } = nativeAndroid;
309+
let measurements: AndroidCombinedProfileEvent['measurements'];
310+
if (nativeMeasurements) {
311+
measurements = {};
312+
for (const key of Object.keys(nativeMeasurements)) {
313+
const nativeMeasurement = nativeMeasurements[key];
314+
if (!nativeMeasurement) {
315+
continue;
316+
}
317+
measurements[key] = {
318+
unit: nativeMeasurement.unit,
319+
values: nativeMeasurement.values.map(v => ({
320+
elapsed_since_start_ns: Number(v.elapsed_since_start_ns),
321+
value: v.value,
322+
})),
323+
};
324+
}
325+
}
326+
308327
return {
309-
...nativeAndroid,
328+
...rest,
310329
platform: 'android',
311330
js_profile: hermes.profile,
312331
duration_ns: durationNs.toString(10),
313332
active_thread_id: hermes.transaction.active_thread_id,
333+
...(measurements && Object.keys(measurements).length > 0 && { measurements }),
314334
};
315335
}
316336

packages/core/src/js/profiling/nativeTypes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,14 @@ export interface NativeAndroidProfileEvent {
5757
* Proguard mapping file hash
5858
*/
5959
build_id?: string;
60+
measurements?: Record<
61+
string,
62+
{
63+
unit: string;
64+
values: {
65+
elapsed_since_start_ns: string;
66+
value: number;
67+
}[];
68+
}
69+
>;
6070
}

packages/core/src/js/profiling/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type AndroidCombinedProfileEvent = {
3333
duration_ns: string;
3434
active_thread_id: string;
3535
profilingStartTimestampNs?: number;
36+
measurements?: AndroidProfileEvent['measurements'];
3637
};
3738

3839
/*

packages/core/test/profiling/fixtures.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,30 @@ export function createMockMinimalValidAndroidProfile(): NativeAndroidProfileEven
166166
build_id: 'mocked-build-id',
167167
};
168168
}
169+
170+
export function createMockMinimalValidAndroidProfileWithMeasurements(): NativeAndroidProfileEvent {
171+
return {
172+
...createMockMinimalValidAndroidProfile(),
173+
measurements: {
174+
frozen_frame_renders: {
175+
unit: 'nanosecond',
176+
values: [{ elapsed_since_start_ns: '1000000', value: 800000000 }],
177+
},
178+
slow_frame_renders: {
179+
unit: 'nanosecond',
180+
values: [{ elapsed_since_start_ns: '2000000', value: 20000000 }],
181+
},
182+
cpu_usage: {
183+
unit: 'percent',
184+
values: [
185+
{ elapsed_since_start_ns: '0', value: 35.5 },
186+
{ elapsed_since_start_ns: '5000000', value: 42.1 },
187+
],
188+
},
189+
memory_footprint: {
190+
unit: 'byte',
191+
values: [{ elapsed_since_start_ns: '0', value: 104857600 }],
192+
},
193+
},
194+
};
195+
}

packages/core/test/profiling/integration.android.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { AndroidCombinedProfileEvent } from '../../src/js/profiling/types';
22

33
import { createAndroidWithHermesProfile } from '../../src/js/profiling/integration';
4-
import { createMockMinimalValidAndroidProfile, createMockMinimalValidHermesProfileEvent } from './fixtures';
4+
import {
5+
createMockMinimalValidAndroidProfile,
6+
createMockMinimalValidAndroidProfileWithMeasurements,
7+
createMockMinimalValidHermesProfileEvent,
8+
} from './fixtures';
59

610
describe('merge Hermes and Android profiles - createAndroidWithHermesProfile', () => {
711
it('should create Android profile structure with hermes profile', () => {
@@ -49,4 +53,41 @@ describe('merge Hermes and Android profiles - createAndroidWithHermesProfile', (
4953
active_thread_id: '123',
5054
});
5155
});
56+
57+
it('should include measurements when present in native Android profile', () => {
58+
const androidProfile = createMockMinimalValidAndroidProfileWithMeasurements();
59+
const result = createAndroidWithHermesProfile(createMockMinimalValidHermesProfileEvent(), androidProfile, 987);
60+
61+
expect(result.measurements).toEqual({
62+
frozen_frame_renders: {
63+
unit: 'nanosecond',
64+
values: [{ elapsed_since_start_ns: 1000000, value: 800000000 }],
65+
},
66+
slow_frame_renders: {
67+
unit: 'nanosecond',
68+
values: [{ elapsed_since_start_ns: 2000000, value: 20000000 }],
69+
},
70+
cpu_usage: {
71+
unit: 'percent',
72+
values: [
73+
{ elapsed_since_start_ns: 0, value: 35.5 },
74+
{ elapsed_since_start_ns: 5000000, value: 42.1 },
75+
],
76+
},
77+
memory_footprint: {
78+
unit: 'byte',
79+
values: [{ elapsed_since_start_ns: 0, value: 104857600 }],
80+
},
81+
});
82+
});
83+
84+
it('should not include measurements when absent from native Android profile', () => {
85+
const result = createAndroidWithHermesProfile(
86+
createMockMinimalValidHermesProfileEvent(),
87+
createMockMinimalValidAndroidProfile(),
88+
987,
89+
);
90+
91+
expect(result.measurements).toBeUndefined();
92+
});
5293
});

packages/core/test/profiling/utils.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,37 @@ describe('enrichAndroidProfileWithEventContext', () => {
147147
expect(result).not.toBeNull();
148148
expect(result).not.toHaveProperty('profilingStartTimestampNs');
149149
});
150+
151+
test('should include measurements when present', () => {
152+
const measurements = {
153+
cpu_usage: {
154+
unit: 'percent' as const,
155+
values: [
156+
{ elapsed_since_start_ns: 0, value: 35.5 },
157+
{ elapsed_since_start_ns: 5000000, value: 42.1 },
158+
],
159+
},
160+
memory_footprint: {
161+
unit: 'byte' as const,
162+
values: [{ elapsed_since_start_ns: 0, value: 104857600 }],
163+
},
164+
};
165+
const profile = createMockAndroidCombinedProfile({ measurements });
166+
const event = createMockEvent();
167+
168+
const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);
169+
170+
expect(result).not.toBeNull();
171+
expect(result!.measurements).toEqual(measurements);
172+
});
173+
174+
test('should not include measurements when absent', () => {
175+
const profile = createMockAndroidCombinedProfile();
176+
const event = createMockEvent();
177+
178+
const result = enrichAndroidProfileWithEventContext('profile-id', profile, event);
179+
180+
expect(result).not.toBeNull();
181+
expect(result!.measurements).toBeUndefined();
182+
});
150183
});

0 commit comments

Comments
 (0)