Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): Add RN/Android mixed profiles #3397

Merged
merged 72 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
7dff905
feat(profiling): Send debug information with Profiles
krystofwoldrich Oct 11, 2023
f147e93
fix white space
krystofwoldrich Oct 11, 2023
5561dc7
Add support for Hermes bytecode frames
krystofwoldrich Oct 13, 2023
fd0876e
Add changelog
krystofwoldrich Oct 13, 2023
7e964ce
Use bundle filename to lookup debugid
krystofwoldrich Oct 13, 2023
073450f
Remove unnecessary lookup logic as only one bundle is supported
krystofwoldrich Oct 13, 2023
22eabf0
Merge branch 'kw-add-hermes-profile-bytecode-frames-parsing' into kw-…
krystofwoldrich Oct 13, 2023
affcee8
Fix build
krystofwoldrich Oct 13, 2023
143d6e2
Merge branch 'kw-add-hermes-profile-bytecode-frames-parsing' into kw-…
krystofwoldrich Oct 13, 2023
5dd4ea7
Fix build
krystofwoldrich Oct 13, 2023
81c2acb
fix lint
krystofwoldrich Oct 13, 2023
c3683f4
Merge branch 'kw-add-hermes-profile-bytecode-frames-parsing' into kw-…
krystofwoldrich Oct 13, 2023
729876e
fix lint
krystofwoldrich Oct 13, 2023
7d608fb
change profile platform to javascript
krystofwoldrich Oct 13, 2023
102f1f1
Mark root as in app false
krystofwoldrich Oct 13, 2023
f412a9e
Add new profile event handling and basic merge
krystofwoldrich Oct 17, 2023
9d8ef0a
fix merging transactions on profile, fix stacks id offset
krystofwoldrich Oct 17, 2023
c778e33
Use sound types, better performance
krystofwoldrich Oct 17, 2023
e04fb7b
Add platform label to native profiles frames
krystofwoldrich Oct 18, 2023
7e33cfa
Use abs_path like error stacktrace frames
krystofwoldrich Oct 18, 2023
66c2d58
Merge remote-tracking branch 'origin/main' into kw-symbolicated-profiles
krystofwoldrich Oct 18, 2023
4b1c004
Fix tests
krystofwoldrich Oct 18, 2023
3b73a95
Merge branch 'kw-symbolicated-profiles' into kw-mixed-profiling
krystofwoldrich Oct 19, 2023
facf3ed
remove duplicate types
krystofwoldrich Oct 19, 2023
f756790
Merge branch 'kw-symbolicated-profiles' into kw-mixed-profiling
krystofwoldrich Oct 19, 2023
5fde434
fix ios codegen
krystofwoldrich Oct 19, 2023
3e900c6
TMP: Fix sentry/types
krystofwoldrich Oct 23, 2023
be6a583
Fix overwritten native debug meta images
krystofwoldrich Oct 24, 2023
fff24a0
Merge remote-tracking branch 'origin/main' into kw-symbolicated-profiles
krystofwoldrich Oct 27, 2023
b1280fd
Merge branch 'kw-symbolicated-profiles' into kw-mixed-profiling
krystofwoldrich Oct 27, 2023
07d8431
Revert "TMP: Fix sentry/types"
krystofwoldrich Oct 27, 2023
0dcaa07
Update changelog
krystofwoldrich Oct 27, 2023
4a8102f
Merge branch 'kw-symbolicated-profiles' into kw-mixed-profiling
krystofwoldrich Oct 27, 2023
497cd85
Add mixed profiles tests
krystofwoldrich Oct 27, 2023
f480212
Add merge native and soruce maps images test
krystofwoldrich Oct 27, 2023
c661623
Merge branch 'main' into kw-mixed-profiling
krystofwoldrich Oct 27, 2023
c00bcca
Add changelog
krystofwoldrich Oct 27, 2023
5d46271
WIP! Add AndroidProfiler
krystofwoldrich Oct 31, 2023
ae6908e
Merge branch 'main' into kw-mixed-profiling
krystofwoldrich Nov 3, 2023
76fb52a
Fix changelog
krystofwoldrich Nov 3, 2023
4b49b9e
Handle native profile trace id safe
krystofwoldrich Nov 3, 2023
a036f0e
Add log when native can't be started
krystofwoldrich Nov 3, 2023
1809241
Merge branch 'kw-mixed-profiling' into kw-android-mixed-profiling
krystofwoldrich Nov 10, 2023
b1b5d03
chore: update scripts/update-android.sh to 6.33.1
web-flow Nov 10, 2023
f3e83da
Merge remote-tracking branch 'origin/deps/scripts/update-android.sh' …
krystofwoldrich Nov 10, 2023
a9034a5
feat(profiling): Add RN and Android mixed profiles
krystofwoldrich Nov 10, 2023
bcd4707
WIP! Add Android and Hermes profiles merge
krystofwoldrich Nov 10, 2023
0740fd0
Add android profiling events with js profile field
krystofwoldrich Nov 14, 2023
a351c6f
Add missing version fields to android profile
krystofwoldrich Nov 15, 2023
9d1bd90
Apply suggestions from code review
krystofwoldrich Nov 15, 2023
dd48ffd
Merge branch 'main' into kw-mixed-profiling
krystofwoldrich Nov 15, 2023
4e85b3d
use sentry-cocoa profiling conditionals
krystofwoldrich Nov 15, 2023
c997508
Merge remote-tracking branch 'origin/kw-mixed-profiling' into kw-mixe…
krystofwoldrich Nov 15, 2023
1b2556b
fix changelog
krystofwoldrich Nov 15, 2023
7fa07d7
Check if hermes and native profiles are nil
krystofwoldrich Nov 15, 2023
665f1d7
add undefined for native profile in js
krystofwoldrich Nov 15, 2023
dce9b4e
Merge branch 'kw-mixed-profiling' into kw-android-mixed-profiling
krystofwoldrich Nov 15, 2023
3b56027
add android profiles tests
krystofwoldrich Nov 16, 2023
044e1f5
Merge remote-tracking branch 'origin/main' into kw-android-mixed-prof…
krystofwoldrich Nov 17, 2023
ac752e1
Add JS debug meta images
krystofwoldrich Nov 17, 2023
fe041f1
Add proguard uuid
krystofwoldrich Nov 17, 2023
65e36a7
Merge branch 'main' into kw-android-mixed-profiling
krystofwoldrich Nov 29, 2023
04a6259
fix ts types
krystofwoldrich Nov 29, 2023
4759050
Add max profile size
krystofwoldrich Dec 4, 2023
b078c12
Merge remote-tracking branch 'origin/main' into kw-android-mixed-prof…
krystofwoldrich Dec 5, 2023
a181842
Make Release build of sample app debuggable
krystofwoldrich Dec 5, 2023
e44ecf5
Fix missing build_id use assets debug meta loader
krystofwoldrich Dec 5, 2023
3294806
Merge branch 'main' into kw-android-mixed-profiling
krystofwoldrich Jan 30, 2024
2ce4fe7
fix profiler sample rate
krystofwoldrich Jan 31, 2024
a19387d
fix changelog
krystofwoldrich Jan 31, 2024
66f2c44
Merge branch 'main' into kw-android-mixed-profiling
krystofwoldrich Feb 8, 2024
1056f4a
Update CHANGELOG.md
krystofwoldrich Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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;
}
Loading
Loading