Skip to content

Commit

Permalink
feat(profiling): Add Native iOS profiles to Hermes JS profiles (#3349)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Nov 17, 2023
1 parent cbb32a7 commit e9fea85
Show file tree
Hide file tree
Showing 15 changed files with 815 additions and 292 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

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

## 5.13.0

### Features
Expand Down
77 changes: 70 additions & 7 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
#import "RCTConvert.h"
#endif

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

#import <Sentry/Sentry.h>
#import <Sentry/PrivateSentrySDKOnly.h>
#import <Sentry/SentryScreenFrames.h>
#import <Sentry/SentryOptions+HybridSDKs.h>
#import <Sentry/SentryBinaryImageCache.h>
#import <Sentry/SentryDependencyContainer.h>
#import <Sentry/SentryFormatter.h>

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

// This guard prevents importing Hermes in JSC apps
#if SENTRY_PROFILING_ENABLED
Expand Down Expand Up @@ -594,16 +597,40 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
}

static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";
static SentryId* nativeProfileTraceId = nil;
static uint64_t nativeProfileStartTime = 0;

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, startProfiling)
{
#if SENTRY_PROFILING_ENABLED
try {
facebook::hermes::HermesRuntime::enableSamplingProfiler();
if (nativeProfileTraceId == nil && nativeProfileStartTime == 0) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
nativeProfileTraceId = [[SentryId alloc] init];
nativeProfileStartTime = [PrivateSentrySDKOnly startProfilerForTrace: nativeProfileTraceId];
#endif
} else {
NSLog(@"Native profiling already in progress. Currently existing trace: %@", nativeProfileTraceId);
}
return @{ @"started": @YES };
} catch (const std::exception& ex) {
if (nativeProfileTraceId != nil) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
#endif
nativeProfileTraceId = nil;
}
nativeProfileStartTime = 0;
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
} catch (...) {
if (nativeProfileTraceId != nil) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
#endif
nativeProfileTraceId = nil;
}
nativeProfileStartTime = 0;
return @{ @"error": @"Failed to start profiling" };
}
#else
Expand All @@ -615,6 +642,17 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
{
#if SENTRY_PROFILING_ENABLED
try {
NSDictionary<NSString *, id> * nativeProfile = nil;
if (nativeProfileTraceId != nil && nativeProfileStartTime != 0) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
uint64_t nativeProfileStopTime = [[[SentryDependencyContainer sharedInstance] dateProvider] systemTime];
nativeProfile = [PrivateSentrySDKOnly collectProfileBetween:nativeProfileStartTime and:nativeProfileStopTime forTrace:nativeProfileTraceId];
#endif
}
// Cleanup native profiles
nativeProfileTraceId = nil;
nativeProfileStartTime = 0;

facebook::hermes::HermesRuntime::disableSamplingProfiler();
std::stringstream ss;
facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);
Expand All @@ -633,10 +671,35 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
}
#endif

return @{ @"profile": data };
if (data == nil) {
return @{ @"error": @"Failed to retrieve Hermes profile." };
}

if (nativeProfile == nil) {
return @{ @"profile": data };
}

return @{
@"profile": data,
@"nativeProfile": nativeProfile,
};
} catch (const std::exception& ex) {
if (nativeProfileTraceId != nil) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
#endif
nativeProfileTraceId = nil;
}
nativeProfileStartTime = 0;
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
} catch (...) {
if (nativeProfileTraceId != nil) {
#if SENTRY_TARGET_PROFILING_SUPPORTED
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
#endif
nativeProfileTraceId = nil;
}
nativeProfileStartTime = 0;
return @{ @"error": @"Failed to stop profiling" };
}
#else
Expand Down
2 changes: 1 addition & 1 deletion src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface Spec extends TurboModule {
fetchModules(): Promise<string | undefined | null>;
fetchViewHierarchy(): Promise<number[] | undefined | null>;
startProfiling(): { started?: boolean; error?: string };
stopProfiling(): { profile?: string; error?: string };
stopProfiling(): { profile?: string; nativeProfile?: UnsafeObject; error?: string };
fetchNativePackageName(): Promise<string | undefined | null>;
fetchNativeStackFramesBy(instructionsAddr: number[]): Promise<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 { RawThreadCpuProfile } from './types';
import type { CombinedProfileEvent } from './types';

export const PROFILE_QUEUE = makeFifoCache<string, RawThreadCpuProfile>(20);
export const PROFILE_QUEUE = makeFifoCache<string, CombinedProfileEvent>(20);
37 changes: 37 additions & 0 deletions src/js/profiling/debugid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { DebugImage } from '@sentry/types';
import { GLOBAL_OBJ, logger } from '@sentry/utils';

import { DEFAULT_BUNDLE_NAME } from './hermes';

/**
* Returns debug meta images of the loaded bundle.
*/
export function getDebugMetadata(): DebugImage[] {
if (!DEFAULT_BUNDLE_NAME) {
return [];
}

const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
if (!debugIdMap) {
return [];
}

const debugIdsKeys = Object.keys(debugIdMap);
if (!debugIdsKeys.length) {
return [];
}

if (debugIdsKeys.length > 1) {
logger.warn(
'[Profiling] Multiple debug images found, but only one one bundle is supported. Using the first one...',
);
}

return [
{
code_file: DEFAULT_BUNDLE_NAME,
debug_id: debugIdMap[debugIdsKeys[0]],
type: 'sourcemap',
},
];
}
29 changes: 0 additions & 29 deletions src/js/profiling/hermes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Platform } from 'react-native';

import { ANDROID_DEFAULT_BUNDLE_NAME, IOS_DEFAULT_BUNDLE_NAME } from '../integrations/rewriteframes';
import { NATIVE } from '../wrapper';
import { convertToSentryProfile } from './convertHermesProfile';
import type { RawThreadCpuProfile } from './types';

export type StackFrameId = number;
export type MicrosecondsSinceBoot = string;
Expand Down Expand Up @@ -55,29 +52,3 @@ export interface Profile {

export const DEFAULT_BUNDLE_NAME =
Platform.OS === 'android' ? ANDROID_DEFAULT_BUNDLE_NAME : Platform.OS === 'ios' ? IOS_DEFAULT_BUNDLE_NAME : undefined;

const MS_TO_NS: number = 1e6;

/**
* Starts Hermes Sampling Profiler and returns the timestamp when profiling started in nanoseconds.
*/
export function startProfiling(): number | null {
const started = NATIVE.startProfiling();
if (!started) {
return null;
}

const profileStartTimestampNs = Date.now() * MS_TO_NS;
return profileStartTimestampNs;
}

/**
* Stops Hermes Sampling Profiler and returns the profile.
*/
export function stopProfiling(): RawThreadCpuProfile | null {
const hermesProfile = NATIVE.stopProfiling();
if (!hermesProfile) {
return null;
}
return convertToSentryProfile(hermesProfile);
}
Loading

0 comments on commit e9fea85

Please sign in to comment.