Skip to content

Commit

Permalink
feat(profiling): Add Hermes JS Profiling (experimental) (#3057)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich committed Jul 27, 2023
1 parent b0855ef commit e4212f4
Show file tree
Hide file tree
Showing 32 changed files with 1,537 additions and 10 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

## Unreleased

### Features

- Alpha support for Hermes JavaScript Profiling ([#3057](https://github.com/getsentry/sentry-react-native/pull/3057))

Profiling is disabled by default. To enable it, configure both
`tracesSampleRate` and `profilesSampleRate` when initializing the SDK:

```javascript
Sentry.init({
dsn: '__DSN__',
tracesSampleRate: 1.0,
_experiments: {
// The sampling rate for profiling is relative to TracesSampleRate.
// In this case, we'll capture profiles for 100% of transactions.
profilesSampleRate: 1.0,
},
});
```

More documentation on profiling and current limitations [can be found here](https://docs.sentry.io/platforms/react-native/profiling/).

### Fixes

- Warn users about multiple versions of `promise` package which can cause unexpected behavior like undefined `Promise.allSettled` ([#3162](https://github.com/getsentry/sentry-react-native/pull/3162))
Expand Down
11 changes: 8 additions & 3 deletions RNSentry.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
require 'json'
version = JSON.parse(File.read('package.json'))["version"]

folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
folly_flags = ' -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32'

is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1"
new_arch_enabled_flag = (is_new_arch_enabled ? folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED" : "")
other_cflags = "$(inherited)" + new_arch_enabled_flag

Pod::Spec.new do |s|
s.name = 'RNSentry'
Expand All @@ -24,9 +29,9 @@ Pod::Spec.new do |s|
s.source_files = 'ios/**/*.{h,mm}'
s.public_header_files = 'ios/RNSentry.h'

s.compiler_flags = other_cflags
# This guard prevent to install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
if is_new_arch_enabled then
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
Expand Down
60 changes: 60 additions & 0 deletions android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import androidx.core.app.FrameMetricsAggregator;

import com.facebook.hermes.instrumentation.HermesSamplingProfiler;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
Expand All @@ -27,7 +28,11 @@
import org.jetbrains.annotations.Nullable;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
Expand Down Expand Up @@ -610,6 +615,61 @@ public void disableNativeFramesTracking() {
}
}

public WritableMap startProfiling() {
final WritableMap result = new WritableNativeMap();
try {
HermesSamplingProfiler.enable();
result.putBoolean("started", true);
} catch (Throwable e) {
result.putBoolean("started", false);
result.putString("error", e.toString());
}
return result;
}

public WritableMap stopProfiling() {
final boolean isDebug = HubAdapter.getInstance().getOptions().isDebug();
final WritableMap result = new WritableNativeMap();
File output = null;
try {
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());

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

result.putString("profile", text.toString());
}
} catch (Throwable e) {
result.putString("error", e.toString());
} finally {
if (output != null) {
try {
final boolean wasProfileSuccessfullyDeleted = output.delete();
if (!wasProfileSuccessfullyDeleted) {
logger.log(SentryLevel.WARNING, "Profile not deleted from:" + output.getAbsolutePath());
}
} catch (Throwable e) {
logger.log(SentryLevel.WARNING, "Profile not deleted from:" + output.getAbsolutePath());
}
}
}
return result;
}

public void fetchNativeDeviceContexts(Promise promise) {
final @NotNull SentryOptions options = HubAdapter.getInstance().getOptions();
if (!(options instanceof SentryAndroidOptions)) {
Expand Down
12 changes: 12 additions & 0 deletions android/src/newarch/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import androidx.annotation.NonNull;

import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;

public class RNSentryModule extends NativeRNSentrySpec {

Expand Down Expand Up @@ -121,4 +123,14 @@ public void fetchNativeDeviceContexts(Promise promise) {
public void fetchNativeSdkInfo(Promise promise) {
this.impl.fetchNativeSdkInfo(promise);
}

@Override
public WritableMap startProfiling() {
return this.impl.startProfiling();
}

@Override
public WritableMap stopProfiling() {
return this.impl.stopProfiling();
}
}
11 changes: 11 additions & 0 deletions android/src/oldarch/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;

public class RNSentryModule extends ReactContextBaseJavaModule {

Expand Down Expand Up @@ -121,4 +122,14 @@ public void fetchNativeDeviceContexts(Promise promise) {
public void fetchNativeSdkInfo(Promise promise) {
this.impl.fetchNativeSdkInfo(promise);
}

@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap startProfiling() {
return this.impl.startProfiling();
}

@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap stopProfiling() {
return this.impl.stopProfiling();
}
}
62 changes: 62 additions & 0 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
#import <Sentry/SentryScreenFrames.h>
#import <Sentry/SentryOptions+HybridSDKs.h>

#if __has_include(<hermes/hermes.h>)
#define SENTRY_PROFILING_ENABLED 1
#else
#define SENTRY_PROFILING_ENABLED 0
#endif

// This guard prevents importing Hermes in JSC apps
#if SENTRY_PROFILING_ENABLED
#import <hermes/hermes.h>
#endif

// Thanks to this guard, we won't import this header when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSentrySpec.h"
Expand Down Expand Up @@ -498,6 +509,57 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
// the 'tracesSampleRate' or 'tracesSampler' option.
}

static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, startProfiling)
{
#if SENTRY_PROFILING_ENABLED
try {
facebook::hermes::HermesRuntime::enableSamplingProfiler();
return @{ @"started": @YES };
} catch (const std::exception& ex) {
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
} catch (...) {
return @{ @"error": @"Failed to start profiling" };
}
#else
return @{ @"error": enabledProfilingMessage };
#endif
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, stopProfiling)
{
#if SENTRY_PROFILING_ENABLED
try {
facebook::hermes::HermesRuntime::disableSamplingProfiler();
std::stringstream ss;
facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);

std::string s = ss.str();
NSString *data = [NSString stringWithCString:s.c_str() encoding:[NSString defaultCStringEncoding]];

#if SENTRY_PROFILING_DEBUG_ENABLED
NSString *rawProfileFileName = @"hermes.profile";
NSError *error = nil;
NSString *rawProfileFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:rawProfileFileName];
if (![data writeToFile:rawProfileFilePath atomically:YES encoding:NSUTF8StringEncoding error:&error]) {
NSLog(@"Error writing Raw Hermes Profile to %@: %@", rawProfileFilePath, error);
} else {
NSLog(@"Raw Hermes Profile saved to %@", rawProfileFilePath);
}
#endif

return @{ @"profile": data };
} catch (const std::exception& ex) {
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
} catch (...) {
return @{ @"error": @"Failed to stop profiling" };
}
#else
return @{ @"error": enabledProfilingMessage };
#endif
}

// Thanks to this guard, we won't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
"test:watch": "jest --watch",
"run-ios": "cd sample-new-architecture && yarn react-native run-ios",
"run-android": "cd sample-new-architecture && yarn react-native run-android"
"run-android": "cd sample-new-architecture && yarn react-native run-android",
"yalc:add:sentry-javascript": "yalc add @sentry/browser @sentry/core @sentry/hub @sentry/integrations @sentry/react @sentry/types @sentry/utils"
},
"keywords": [
"react-native",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package com.samplenewarchitecture;

import android.app.Application;

import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptExecutor;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.common.JavascriptException;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import java.util.List;

import io.sentry.react.RNSentryModuleImpl;
import io.sentry.react.RNSentryPackage;

public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {

@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
Expand Down
3 changes: 2 additions & 1 deletion sample-new-architecture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"pod-install": "cd ios; RCT_NEW_ARCH_ENABLED=1 bundle exec pod install; cd ..",
"pod-install-production": "cd ios; PRODUCTION=1 RCT_NEW_ARCH_ENABLED=1 bundle exec pod install; cd ..",
"pod-install-legacy": "cd ios; bundle exec pod install; cd ..",
"pod-install-legacy-production": "cd ios; PRODUCTION=1 bundle exec pod install; cd .."
"pod-install-legacy-production": "cd ios; PRODUCTION=1 bundle exec pod install; cd ..",
"clean-ios": "cd ios; rm -rf Podfile.lock Pods build; cd .."
},
"dependencies": {
"@react-navigation/native": "^6.1.7",
Expand Down
5 changes: 4 additions & 1 deletion sample-new-architecture/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Sentry.init({
dsn: SENTRY_INTERNAL_DSN,
debug: true,
beforeSend: (event: Sentry.Event) => {
console.log('Event beforeSend:', event);
console.log('Event beforeSend:', event.event_id);
return event;
},
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
Expand Down Expand Up @@ -85,6 +85,9 @@ Sentry.init({
// otherwise they will not work.
// release: '[email protected]+1',
// dist: `1`,
_experiments: {
profilesSampleRate: 1,
},
});

const Stack = createStackNavigator();
Expand Down
1 change: 0 additions & 1 deletion sample-new-architecture/src/Screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ const HomeScreen = (props: Props) => {
/>
)}
<Spacer />

<Sentry.ErrorBoundary fallback={errorBoundaryFallback}>
<Button
title="Activate Error Boundary"
Expand Down
26 changes: 26 additions & 0 deletions scripts/hermes-profile-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { writeFileSync } = require('fs');

const transformer = require('hermes-profile-transformer').default;

const hermesCpuProfilePath = 'iOS_release_profile.json';
const sourceMapPath = './main.jsbundle.map';
const sourceMapBundleFileName = 'main.jsbundle';

transformer(
// profile path is required
hermesCpuProfilePath,
// source maps are optional
sourceMapPath,
sourceMapBundleFileName
)
.then(events => {
// write converted trace to a file
return writeFileSync(
'./chrome-supported.json',
JSON.stringify(events, null, 2),
'utf-8'
);
})
.catch(err => {
console.log(err);
});
2 changes: 2 additions & 0 deletions src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface Spec extends TurboModule {
enableNativeFramesTracking(): void;
fetchModules(): Promise<string | undefined | null>;
fetchViewHierarchy(): Promise<number[] | undefined | null>;
startProfiling(): { started?: boolean; error?: string };
stopProfiling(): { profile?: string; error?: string };
}

export type NativeAppStartResponse = {
Expand Down
2 changes: 2 additions & 0 deletions src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {

let shouldClearOutcomesBuffer = true;
if (this._transport && this._dsn) {
this.emit('beforeEnvelope', envelope);

this._transport.send(envelope).then(null, reason => {
if (reason instanceof SentryError) {
// SentryError is thrown by SyncPromise
Expand Down
1 change: 1 addition & 0 deletions src/js/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { EventOrigin } from './eventorigin';
export { SdkInfo } from './sdkinfo';
export { ReactNativeInfo } from './reactnativeinfo';
export { ModulesLoader } from './modulesloader';
export { HermesProfiling } from '../profiling/integration';
4 changes: 2 additions & 2 deletions src/js/integrations/rewriteframes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Platform } from 'react-native';

import { isExpo } from '../utils/environment';

const ANDROID_DEFAULT_BUNDLE_NAME = 'app:///index.android.bundle';
const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle';
export const ANDROID_DEFAULT_BUNDLE_NAME = 'app:///index.android.bundle';
export const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle';

/**
* Creates React Native default rewrite frames integration
Expand Down
Loading

0 comments on commit e4212f4

Please sign in to comment.