Skip to content

Commit

Permalink
feat(profiling): Add RN/Android mixed profiles (#3397)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich committed Feb 12, 2024
1 parent b282f25 commit 7835531
Show file tree
Hide file tree
Showing 16 changed files with 649 additions and 59 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,10 @@

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).

### Features

- Add Android profiles to React Native Profiling ([#3397](https://github.com/getsentry/sentry-react-native/pull/3397))

### Fixes

- Upload Debug Symbols Build Phase continues when `node` not found in `WITH_ENVIRONMENT` ([#3573](https://github.com/getsentry/sentry-react-native/pull/3573))
Expand Down
112 changes: 99 additions & 13 deletions android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
@@ -1,6 +1,9 @@
package io.sentry.react;

import static java.util.concurrent.TimeUnit.SECONDS;
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
import static io.sentry.vendor.Base64.NO_PADDING;
import static io.sentry.vendor.Base64.NO_WRAP;

import android.app.Activity;
import android.content.Context;
Expand Down Expand Up @@ -28,31 +31,37 @@

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import io.sentry.Breadcrumb;
import io.sentry.DateUtils;
import io.sentry.HubAdapter;
import io.sentry.ILogger;
import io.sentry.ISentryExecutorService;
import io.sentry.IScope;
import io.sentry.ISerializer;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryDate;
import io.sentry.SentryEvent;
import io.sentry.SentryExecutorService;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.android.core.AndroidLogger;
import io.sentry.android.core.AndroidProfiler;
import io.sentry.android.core.AnrIntegration;
import io.sentry.android.core.BuildConfig;
import io.sentry.android.core.BuildInfoProvider;
Expand All @@ -62,14 +71,18 @@
import io.sentry.android.core.SentryAndroid;
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.android.core.ViewHierarchyEventProcessor;
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.protocol.SdkVersion;
import io.sentry.protocol.SentryException;
import io.sentry.protocol.SentryPackage;
import io.sentry.protocol.User;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.util.DebugMetaPropertiesApplier;
import io.sentry.util.JsonSerializationUtils;
import io.sentry.vendor.Base64;
import io.sentry.util.FileUtils;

public class RNSentryModuleImpl {

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

private static final int SCREENSHOT_TIMEOUT_SECONDS = 2;

/**
* Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible
* lockstep sampling. More on
* https://stackoverflow.com/questions/45470758/what-is-lockstep-sampling
*/
private int profilingTracesHz = 101;

private AndroidProfiler androidProfiler = null;

private boolean isProguardDebugMetaLoaded = false;
private @Nullable String proguardUuid = null;
private String cacheDirPath = null;
private ISentryExecutorService executorService = null;

/** Max trace file size in bytes. */
private long maxTraceFileSize = 5 * 1024 * 1024;

public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) {
packageInfo = getPackageInfo(reactApplicationContext);
this.reactApplicationContext = reactApplicationContext;
Expand Down Expand Up @@ -393,7 +423,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) {
}

try {
doneSignal.await(SCREENSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
doneSignal.await(SCREENSHOT_TIMEOUT_SECONDS, SECONDS);
} catch (InterruptedException e) {
logger.log(SentryLevel.ERROR, "Screenshot process was interrupted.");
return null;
Expand Down Expand Up @@ -611,10 +641,41 @@ public void disableNativeFramesTracking() {
}
}

private String getProfilingTracesDirPath() {
if (cacheDirPath == null) {
cacheDirPath = new File(getReactApplicationContext().getCacheDir(), "sentry/react").getAbsolutePath();
}
File profilingTraceDir = new File(cacheDirPath, "profiling_trace");
profilingTraceDir.mkdirs();
return profilingTraceDir.getAbsolutePath();
}

private void initializeAndroidProfiler() {
if (executorService == null) {
executorService = new SentryExecutorService();
}
final String tracesFilesDirPath = getProfilingTracesDirPath();

androidProfiler = new AndroidProfiler(
tracesFilesDirPath,
(int) SECONDS.toMicros(1) / profilingTracesHz,
new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo),
executorService,
logger,
buildInfo
);
}

public WritableMap startProfiling() {
final WritableMap result = new WritableNativeMap();
if (androidProfiler == null) {
initializeAndroidProfiler();
}

try {
HermesSamplingProfiler.enable();
androidProfiler.start();

result.putBoolean("started", true);
} catch (Throwable e) {
result.putBoolean("started", false);
Expand All @@ -628,27 +689,26 @@ public WritableMap stopProfiling() {
final WritableMap result = new WritableNativeMap();
File output = null;
try {
AndroidProfiler.ProfileEndData end = androidProfiler.endAndCollect(false, null);
HermesSamplingProfiler.disable();

output = File.createTempFile(
"sampling-profiler-trace", ".cpuprofile", reactApplicationContext.getCacheDir());

if (isDebug) {
logger.log(SentryLevel.INFO, "Profile saved to: " + output.getAbsolutePath());
}

try (final BufferedReader br = new BufferedReader(new FileReader(output));) {
HermesSamplingProfiler.dumpSampledTraceToFile(output.getPath());
HermesSamplingProfiler.dumpSampledTraceToFile(output.getPath());
result.putString("profile", readStringFromFile(output));

final StringBuilder text = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\n');
}
WritableMap androidProfile = new WritableNativeMap();
byte[] androidProfileBytes = FileUtils.readBytesFromFile(end.traceFile.getPath(), maxTraceFileSize);
String base64AndroidProfile = Base64.encodeToString(androidProfileBytes, NO_WRAP | NO_PADDING);

result.putString("profile", text.toString());
}
androidProfile.putString("sampled_profile", base64AndroidProfile);
androidProfile.putInt("android_api_level", buildInfo.getSdkInfoVersion());
androidProfile.putString("build_id", getProguardUuid());
result.putMap("androidProfile", androidProfile);
} catch (Throwable e) {
result.putString("error", e.toString());
} finally {
Expand All @@ -666,6 +726,32 @@ public WritableMap stopProfiling() {
return result;
}

private @Nullable String getProguardUuid() {
if (isProguardDebugMetaLoaded) {
return proguardUuid;
}
isProguardDebugMetaLoaded = true;
final @Nullable Properties debugMeta = (new AssetsDebugMetaLoader(this.getReactApplicationContext(), logger)).loadDebugMeta();
if (debugMeta != null) {
proguardUuid = DebugMetaPropertiesApplier.getProguardUuid(debugMeta);
return proguardUuid;
}
return null;
}

private String readStringFromFile(File path) throws IOException {
try (final BufferedReader br = new BufferedReader(new FileReader(path));) {

final StringBuilder text = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\n');
}
return text.toString();
}
}

public void fetchNativeDeviceContexts(Promise promise) {
final @NotNull SentryOptions options = HubAdapter.getInstance().getOptions();
if (!(options instanceof SentryAndroidOptions)) {
Expand Down
1 change: 1 addition & 0 deletions samples/react-native/android/app/build.gradle
Expand Up @@ -145,6 +145,7 @@ android {
signingConfig signingConfigs.debug
}
release {
debuggable true
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
Expand Down
2 changes: 1 addition & 1 deletion samples/react-native/src/App.tsx
Expand Up @@ -91,7 +91,7 @@ Sentry.init({
// release: 'myapp@1.2.3+1',
// dist: `1`,
_experiments: {
profilesSampleRate: 0,
profilesSampleRate: 1.0,
},
enableSpotlight: true,
});
Expand Down
7 changes: 6 additions & 1 deletion src/js/NativeRNSentry.ts
Expand Up @@ -33,7 +33,12 @@ export interface Spec extends TurboModule {
fetchModules(): Promise<string | undefined | null>;
fetchViewHierarchy(): Promise<number[] | undefined | null>;
startProfiling(): { started?: boolean; error?: string };
stopProfiling(): { profile?: string; nativeProfile?: UnsafeObject; error?: string };
stopProfiling(): {
profile?: string;
nativeProfile?: UnsafeObject;
androidProfile?: UnsafeObject;
error?: string;
};
fetchNativePackageName(): string | undefined | null;
fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/js/profiling/cache.ts
@@ -1,5 +1,5 @@
import { makeFifoCache } from '@sentry/utils';

import type { CombinedProfileEvent } from './types';
import type { AndroidCombinedProfileEvent, CombinedProfileEvent } from './types';

export const PROFILE_QUEUE = makeFifoCache<string, CombinedProfileEvent>(20);
export const PROFILE_QUEUE = makeFifoCache<string, CombinedProfileEvent | AndroidCombinedProfileEvent>(20);
51 changes: 33 additions & 18 deletions src/js/profiling/integration.ts
@@ -1,15 +1,7 @@
/* eslint-disable complexity */
import type { Hub } from '@sentry/core';
import { getActiveTransaction } from '@sentry/core';
import type {
Envelope,
Event,
EventProcessor,
Integration,
Profile,
ThreadCpuProfile,
Transaction,
} from '@sentry/types';
import type { Envelope, Event, EventProcessor, Integration, ThreadCpuProfile, Transaction } from '@sentry/types';
import { logger, uuid4 } from '@sentry/utils';
import { Platform } from 'react-native';

Expand All @@ -18,8 +10,8 @@ import { NATIVE } from '../wrapper';
import { PROFILE_QUEUE } from './cache';
import { MAX_PROFILE_DURATION_MS } from './constants';
import { convertToSentryProfile } from './convertHermesProfile';
import type { NativeProfileEvent } from './nativeTypes';
import type { CombinedProfileEvent, HermesProfileEvent } from './types';
import type { NativeAndroidProfileEvent, NativeProfileEvent } from './nativeTypes';
import type { AndroidCombinedProfileEvent, CombinedProfileEvent, HermesProfileEvent, ProfileEvent } from './types';
import {
addProfilesToEnvelope,
createHermesProfilingEvent,
Expand Down Expand Up @@ -88,7 +80,7 @@ export class HermesProfiling implements Integration {
return;
}

const profilesToAddToEnvelope: Profile[] = [];
const profilesToAddToEnvelope: ProfileEvent[] = [];
for (const profiledTransaction of profiledTransactions) {
const profile = this._createProfileEventFor(profiledTransaction);
if (profile) {
Expand Down Expand Up @@ -174,7 +166,7 @@ export class HermesProfiling implements Integration {
return;
}

const profile = stopProfiling();
const profile = stopProfiling(this._currentProfile.startTimestampNs);
if (!profile) {
logger.warn('[Profiling] Stop failed. Cleaning up...');
this._currentProfile = undefined;
Expand All @@ -187,7 +179,7 @@ export class HermesProfiling implements Integration {
this._currentProfile = undefined;
};

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

if (typeof profile_id !== 'string') {
Expand Down Expand Up @@ -235,11 +227,14 @@ export function startProfiling(): number | null {
/**
* Stops Profilers and returns collected combined profile.
*/
export function stopProfiling(): CombinedProfileEvent | null {
export function stopProfiling(
profileStartTimestampNs: number,
): CombinedProfileEvent | AndroidCombinedProfileEvent | null {
const collectedProfiles = NATIVE.stopProfiling();
if (!collectedProfiles) {
return null;
}
const profileEndTimestampNs = Date.now() * MS_TO_NS;

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

if (!collectedProfiles.nativeProfile) {
return hermesProfileEvent;
if (collectedProfiles.androidProfile) {
const durationNs = profileEndTimestampNs - profileStartTimestampNs;
return createAndroidWithHermesProfile(hermesProfileEvent, collectedProfiles.androidProfile, durationNs);
} else if (collectedProfiles.nativeProfile) {
return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
}

return addNativeProfileToHermesProfile(hermesProfileEvent, collectedProfiles.nativeProfile);
return hermesProfileEvent;
}

/**
* Creates Android profile event with attached javascript profile.
*/
export function createAndroidWithHermesProfile(
hermes: HermesProfileEvent,
nativeAndroid: NativeAndroidProfileEvent,
durationNs: number,
): AndroidCombinedProfileEvent {
return {
...nativeAndroid,
platform: 'android',
js_profile: hermes.profile,
duration_ns: durationNs.toString(10),
active_thread_id: hermes.transaction.active_thread_id,
};
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/js/profiling/nativeTypes.ts
Expand Up @@ -49,3 +49,12 @@ export interface NativeProfileEvent {
}[];
};
}

export interface NativeAndroidProfileEvent {
sampled_profile: string;
android_api_level: number;
/**
* Proguard mapping file hash
*/
build_id?: string;
}

0 comments on commit 7835531

Please sign in to comment.