Skip to content

Commit 7835531

Browse files
feat(profiling): Add RN/Android mixed profiles (#3397)
1 parent b282f25 commit 7835531

File tree

16 files changed

+649
-59
lines changed

16 files changed

+649
-59
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
This release contains upgrade of `sentry-android` dependency to major version 7. There are no breaking changes in the JS API. If you are using the Android API please check [the migration guide](https://docs.sentry.io/platforms/android/migration/#migrating-from-iosentrysentry-android-6x-to-iosentrysentry-android-700).
66

7+
### Features
8+
9+
- Add Android profiles to React Native Profiling ([#3397](https://github.com/getsentry/sentry-react-native/pull/3397))
10+
711
### Fixes
812

913
- Upload Debug Symbols Build Phase continues when `node` not found in `WITH_ENVIRONMENT` ([#3573](https://github.com/getsentry/sentry-react-native/pull/3573))

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

+99-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.sentry.react;
22

3+
import static java.util.concurrent.TimeUnit.SECONDS;
34
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
5+
import static io.sentry.vendor.Base64.NO_PADDING;
6+
import static io.sentry.vendor.Base64.NO_WRAP;
47

58
import android.app.Activity;
69
import android.content.Context;
@@ -28,31 +31,37 @@
2831

2932
import java.io.BufferedInputStream;
3033
import java.io.BufferedReader;
34+
import java.io.ByteArrayOutputStream;
3135
import java.io.File;
36+
import java.io.FileInputStream;
3237
import java.io.FileNotFoundException;
3338
import java.io.FileReader;
39+
import java.io.IOException;
3440
import java.io.InputStream;
3541
import java.nio.charset.Charset;
3642
import java.util.HashMap;
3743
import java.util.List;
3844
import java.util.Map;
45+
import java.util.Properties;
3946
import java.util.concurrent.CountDownLatch;
40-
import java.util.concurrent.TimeUnit;
4147

4248
import io.sentry.Breadcrumb;
4349
import io.sentry.DateUtils;
4450
import io.sentry.HubAdapter;
4551
import io.sentry.ILogger;
52+
import io.sentry.ISentryExecutorService;
4653
import io.sentry.IScope;
4754
import io.sentry.ISerializer;
4855
import io.sentry.Integration;
4956
import io.sentry.Sentry;
5057
import io.sentry.SentryDate;
5158
import io.sentry.SentryEvent;
59+
import io.sentry.SentryExecutorService;
5260
import io.sentry.SentryLevel;
5361
import io.sentry.SentryOptions;
5462
import io.sentry.UncaughtExceptionHandlerIntegration;
5563
import io.sentry.android.core.AndroidLogger;
64+
import io.sentry.android.core.AndroidProfiler;
5665
import io.sentry.android.core.AnrIntegration;
5766
import io.sentry.android.core.BuildConfig;
5867
import io.sentry.android.core.BuildInfoProvider;
@@ -62,14 +71,18 @@
6271
import io.sentry.android.core.SentryAndroid;
6372
import io.sentry.android.core.SentryAndroidOptions;
6473
import io.sentry.android.core.ViewHierarchyEventProcessor;
74+
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
75+
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
6576
import io.sentry.android.core.performance.AppStartMetrics;
6677
import io.sentry.protocol.SdkVersion;
6778
import io.sentry.protocol.SentryException;
6879
import io.sentry.protocol.SentryPackage;
6980
import io.sentry.protocol.User;
7081
import io.sentry.protocol.ViewHierarchy;
82+
import io.sentry.util.DebugMetaPropertiesApplier;
7183
import io.sentry.util.JsonSerializationUtils;
7284
import io.sentry.vendor.Base64;
85+
import io.sentry.util.FileUtils;
7386

7487
public class RNSentryModuleImpl {
7588

@@ -96,6 +109,23 @@ public class RNSentryModuleImpl {
96109

97110
private static final int SCREENSHOT_TIMEOUT_SECONDS = 2;
98111

112+
/**
113+
* Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible
114+
* lockstep sampling. More on
115+
* https://stackoverflow.com/questions/45470758/what-is-lockstep-sampling
116+
*/
117+
private int profilingTracesHz = 101;
118+
119+
private AndroidProfiler androidProfiler = null;
120+
121+
private boolean isProguardDebugMetaLoaded = false;
122+
private @Nullable String proguardUuid = null;
123+
private String cacheDirPath = null;
124+
private ISentryExecutorService executorService = null;
125+
126+
/** Max trace file size in bytes. */
127+
private long maxTraceFileSize = 5 * 1024 * 1024;
128+
99129
public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) {
100130
packageInfo = getPackageInfo(reactApplicationContext);
101131
this.reactApplicationContext = reactApplicationContext;
@@ -393,7 +423,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) {
393423
}
394424

395425
try {
396-
doneSignal.await(SCREENSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
426+
doneSignal.await(SCREENSHOT_TIMEOUT_SECONDS, SECONDS);
397427
} catch (InterruptedException e) {
398428
logger.log(SentryLevel.ERROR, "Screenshot process was interrupted.");
399429
return null;
@@ -611,10 +641,41 @@ public void disableNativeFramesTracking() {
611641
}
612642
}
613643

644+
private String getProfilingTracesDirPath() {
645+
if (cacheDirPath == null) {
646+
cacheDirPath = new File(getReactApplicationContext().getCacheDir(), "sentry/react").getAbsolutePath();
647+
}
648+
File profilingTraceDir = new File(cacheDirPath, "profiling_trace");
649+
profilingTraceDir.mkdirs();
650+
return profilingTraceDir.getAbsolutePath();
651+
}
652+
653+
private void initializeAndroidProfiler() {
654+
if (executorService == null) {
655+
executorService = new SentryExecutorService();
656+
}
657+
final String tracesFilesDirPath = getProfilingTracesDirPath();
658+
659+
androidProfiler = new AndroidProfiler(
660+
tracesFilesDirPath,
661+
(int) SECONDS.toMicros(1) / profilingTracesHz,
662+
new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo),
663+
executorService,
664+
logger,
665+
buildInfo
666+
);
667+
}
668+
614669
public WritableMap startProfiling() {
615670
final WritableMap result = new WritableNativeMap();
671+
if (androidProfiler == null) {
672+
initializeAndroidProfiler();
673+
}
674+
616675
try {
617676
HermesSamplingProfiler.enable();
677+
androidProfiler.start();
678+
618679
result.putBoolean("started", true);
619680
} catch (Throwable e) {
620681
result.putBoolean("started", false);
@@ -628,27 +689,26 @@ public WritableMap stopProfiling() {
628689
final WritableMap result = new WritableNativeMap();
629690
File output = null;
630691
try {
692+
AndroidProfiler.ProfileEndData end = androidProfiler.endAndCollect(false, null);
631693
HermesSamplingProfiler.disable();
632694

633695
output = File.createTempFile(
634696
"sampling-profiler-trace", ".cpuprofile", reactApplicationContext.getCacheDir());
635-
636697
if (isDebug) {
637698
logger.log(SentryLevel.INFO, "Profile saved to: " + output.getAbsolutePath());
638699
}
639700

640-
try (final BufferedReader br = new BufferedReader(new FileReader(output));) {
641-
HermesSamplingProfiler.dumpSampledTraceToFile(output.getPath());
701+
HermesSamplingProfiler.dumpSampledTraceToFile(output.getPath());
702+
result.putString("profile", readStringFromFile(output));
642703

643-
final StringBuilder text = new StringBuilder();
644-
String line;
645-
while ((line = br.readLine()) != null) {
646-
text.append(line);
647-
text.append('\n');
648-
}
704+
WritableMap androidProfile = new WritableNativeMap();
705+
byte[] androidProfileBytes = FileUtils.readBytesFromFile(end.traceFile.getPath(), maxTraceFileSize);
706+
String base64AndroidProfile = Base64.encodeToString(androidProfileBytes, NO_WRAP | NO_PADDING);
649707

650-
result.putString("profile", text.toString());
651-
}
708+
androidProfile.putString("sampled_profile", base64AndroidProfile);
709+
androidProfile.putInt("android_api_level", buildInfo.getSdkInfoVersion());
710+
androidProfile.putString("build_id", getProguardUuid());
711+
result.putMap("androidProfile", androidProfile);
652712
} catch (Throwable e) {
653713
result.putString("error", e.toString());
654714
} finally {
@@ -666,6 +726,32 @@ public WritableMap stopProfiling() {
666726
return result;
667727
}
668728

729+
private @Nullable String getProguardUuid() {
730+
if (isProguardDebugMetaLoaded) {
731+
return proguardUuid;
732+
}
733+
isProguardDebugMetaLoaded = true;
734+
final @Nullable Properties debugMeta = (new AssetsDebugMetaLoader(this.getReactApplicationContext(), logger)).loadDebugMeta();
735+
if (debugMeta != null) {
736+
proguardUuid = DebugMetaPropertiesApplier.getProguardUuid(debugMeta);
737+
return proguardUuid;
738+
}
739+
return null;
740+
}
741+
742+
private String readStringFromFile(File path) throws IOException {
743+
try (final BufferedReader br = new BufferedReader(new FileReader(path));) {
744+
745+
final StringBuilder text = new StringBuilder();
746+
String line;
747+
while ((line = br.readLine()) != null) {
748+
text.append(line);
749+
text.append('\n');
750+
}
751+
return text.toString();
752+
}
753+
}
754+
669755
public void fetchNativeDeviceContexts(Promise promise) {
670756
final @NotNull SentryOptions options = HubAdapter.getInstance().getOptions();
671757
if (!(options instanceof SentryAndroidOptions)) {

samples/react-native/android/app/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ android {
145145
signingConfig signingConfigs.debug
146146
}
147147
release {
148+
debuggable true
148149
// Caution! In production, you need to generate your own keystore file.
149150
// see https://reactnative.dev/docs/signed-apk-android.
150151
signingConfig signingConfigs.debug

samples/react-native/src/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Sentry.init({
9191
// release: '[email protected]+1',
9292
// dist: `1`,
9393
_experiments: {
94-
profilesSampleRate: 0,
94+
profilesSampleRate: 1.0,
9595
},
9696
enableSpotlight: true,
9797
});

src/js/NativeRNSentry.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ export interface Spec extends TurboModule {
3333
fetchModules(): Promise<string | undefined | null>;
3434
fetchViewHierarchy(): Promise<number[] | undefined | null>;
3535
startProfiling(): { started?: boolean; error?: string };
36-
stopProfiling(): { profile?: string; nativeProfile?: UnsafeObject; error?: string };
36+
stopProfiling(): {
37+
profile?: string;
38+
nativeProfile?: UnsafeObject;
39+
androidProfile?: UnsafeObject;
40+
error?: string;
41+
};
3742
fetchNativePackageName(): string | undefined | null;
3843
fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null;
3944
}

src/js/profiling/cache.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { makeFifoCache } from '@sentry/utils';
22

3-
import type { CombinedProfileEvent } from './types';
3+
import type { AndroidCombinedProfileEvent, CombinedProfileEvent } from './types';
44

5-
export const PROFILE_QUEUE = makeFifoCache<string, CombinedProfileEvent>(20);
5+
export const PROFILE_QUEUE = makeFifoCache<string, CombinedProfileEvent | AndroidCombinedProfileEvent>(20);

src/js/profiling/integration.ts

+33-18
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
/* eslint-disable complexity */
22
import type { Hub } from '@sentry/core';
33
import { getActiveTransaction } from '@sentry/core';
4-
import type {
5-
Envelope,
6-
Event,
7-
EventProcessor,
8-
Integration,
9-
Profile,
10-
ThreadCpuProfile,
11-
Transaction,
12-
} from '@sentry/types';
4+
import type { Envelope, Event, EventProcessor, Integration, ThreadCpuProfile, Transaction } from '@sentry/types';
135
import { logger, uuid4 } from '@sentry/utils';
146
import { Platform } from 'react-native';
157

@@ -18,8 +10,8 @@ import { NATIVE } from '../wrapper';
1810
import { PROFILE_QUEUE } from './cache';
1911
import { MAX_PROFILE_DURATION_MS } from './constants';
2012
import { convertToSentryProfile } from './convertHermesProfile';
21-
import type { NativeProfileEvent } from './nativeTypes';
22-
import type { CombinedProfileEvent, HermesProfileEvent } from './types';
13+
import type { NativeAndroidProfileEvent, NativeProfileEvent } from './nativeTypes';
14+
import type { AndroidCombinedProfileEvent, CombinedProfileEvent, HermesProfileEvent, ProfileEvent } from './types';
2315
import {
2416
addProfilesToEnvelope,
2517
createHermesProfilingEvent,
@@ -88,7 +80,7 @@ export class HermesProfiling implements Integration {
8880
return;
8981
}
9082

91-
const profilesToAddToEnvelope: Profile[] = [];
83+
const profilesToAddToEnvelope: ProfileEvent[] = [];
9284
for (const profiledTransaction of profiledTransactions) {
9385
const profile = this._createProfileEventFor(profiledTransaction);
9486
if (profile) {
@@ -174,7 +166,7 @@ export class HermesProfiling implements Integration {
174166
return;
175167
}
176168

177-
const profile = stopProfiling();
169+
const profile = stopProfiling(this._currentProfile.startTimestampNs);
178170
if (!profile) {
179171
logger.warn('[Profiling] Stop failed. Cleaning up...');
180172
this._currentProfile = undefined;
@@ -187,7 +179,7 @@ export class HermesProfiling implements Integration {
187179
this._currentProfile = undefined;
188180
};
189181

190-
private _createProfileEventFor = (profiledTransaction: Event): Profile | null => {
182+
private _createProfileEventFor = (profiledTransaction: Event): ProfileEvent | null => {
191183
const profile_id = profiledTransaction?.contexts?.['profile']?.['profile_id'];
192184

193185
if (typeof profile_id !== 'string') {
@@ -235,11 +227,14 @@ export function startProfiling(): number | null {
235227
/**
236228
* Stops Profilers and returns collected combined profile.
237229
*/
238-
export function stopProfiling(): CombinedProfileEvent | null {
230+
export function stopProfiling(
231+
profileStartTimestampNs: number,
232+
): CombinedProfileEvent | AndroidCombinedProfileEvent | null {
239233
const collectedProfiles = NATIVE.stopProfiling();
240234
if (!collectedProfiles) {
241235
return null;
242236
}
237+
const profileEndTimestampNs = Date.now() * MS_TO_NS;
243238

244239
const hermesProfile = convertToSentryProfile(collectedProfiles.hermesProfile);
245240
if (!hermesProfile) {
@@ -251,11 +246,31 @@ export function stopProfiling(): CombinedProfileEvent | null {
251246
return null;
252247
}
253248

254-
if (!collectedProfiles.nativeProfile) {
255-
return hermesProfileEvent;
249+
if (collectedProfiles.androidProfile) {
250+
const durationNs = profileEndTimestampNs - profileStartTimestampNs;
251+
return createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs);
252+
} else if (collectedProfiles.nativeProfile) {
253+
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
256254
}
257255

258-
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
256+
return hermesProfileEvent;
257+
}
258+
259+
/**
260+
* Creates Android profile event with attached javascript profile.
261+
*/
262+
export function createAndroidWithHermesProfile(
263+
hermes: HermesProfileEvent,
264+
nativeAndroid: NativeAndroidProfileEvent,
265+
durationNs: number,
266+
): AndroidCombinedProfileEvent {
267+
return {
268+
...nativeAndroid,
269+
platform: 'android',
270+
js_profile: hermes.profile,
271+
duration_ns: durationNs.toString(10),
272+
active_thread_id: hermes.transaction.active_thread_id,
273+
};
259274
}
260275

261276
/**

src/js/profiling/nativeTypes.ts

+9
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,12 @@ export interface NativeProfileEvent {
4949
}[];
5050
};
5151
}
52+
53+
export interface NativeAndroidProfileEvent {
54+
sampled_profile: string;
55+
android_api_level: number;
56+
/**
57+
* Proguard mapping file hash
58+
*/
59+
build_id?: string;
60+
}

0 commit comments

Comments
 (0)