Skip to content

Commit e9fea85

Browse files
feat(profiling): Add Native iOS profiles to Hermes JS profiles (#3349)
1 parent cbb32a7 commit e9fea85

File tree

15 files changed

+815
-292
lines changed

15 files changed

+815
-292
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add iOS profiles to React Native Profiling ([#3349](https://github.com/getsentry/sentry-react-native/pull/3349))
8+
39
## 5.13.0
410

511
### Features

ios/RNSentry.mm

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@
77
#import "RCTConvert.h"
88
#endif
99

10+
#if __has_include(<hermes/hermes.h>)
11+
#define SENTRY_PROFILING_ENABLED 1
12+
#import <Sentry/SentryProfilingConditionals.h>
13+
#else
14+
#define SENTRY_PROFILING_ENABLED 0
15+
#define SENTRY_TARGET_PROFILING_SUPPORTED 0
16+
#endif
17+
1018
#import <Sentry/Sentry.h>
1119
#import <Sentry/PrivateSentrySDKOnly.h>
1220
#import <Sentry/SentryScreenFrames.h>
1321
#import <Sentry/SentryOptions+HybridSDKs.h>
1422
#import <Sentry/SentryBinaryImageCache.h>
1523
#import <Sentry/SentryDependencyContainer.h>
1624
#import <Sentry/SentryFormatter.h>
17-
18-
#if __has_include(<hermes/hermes.h>)
19-
#define SENTRY_PROFILING_ENABLED 1
20-
#else
21-
#define SENTRY_PROFILING_ENABLED 0
22-
#endif
25+
#import <Sentry/SentryCurrentDateProvider.h>
2326

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

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

598603
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, startProfiling)
599604
{
600605
#if SENTRY_PROFILING_ENABLED
601606
try {
602607
facebook::hermes::HermesRuntime::enableSamplingProfiler();
608+
if (nativeProfileTraceId == nil && nativeProfileStartTime == 0) {
609+
#if SENTRY_TARGET_PROFILING_SUPPORTED
610+
nativeProfileTraceId = [[SentryId alloc] init];
611+
nativeProfileStartTime = [PrivateSentrySDKOnly startProfilerForTrace: nativeProfileTraceId];
612+
#endif
613+
} else {
614+
NSLog(@"Native profiling already in progress. Currently existing trace: %@", nativeProfileTraceId);
615+
}
603616
return @{ @"started": @YES };
604617
} catch (const std::exception& ex) {
618+
if (nativeProfileTraceId != nil) {
619+
#if SENTRY_TARGET_PROFILING_SUPPORTED
620+
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
621+
#endif
622+
nativeProfileTraceId = nil;
623+
}
624+
nativeProfileStartTime = 0;
605625
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
606626
} catch (...) {
627+
if (nativeProfileTraceId != nil) {
628+
#if SENTRY_TARGET_PROFILING_SUPPORTED
629+
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
630+
#endif
631+
nativeProfileTraceId = nil;
632+
}
633+
nativeProfileStartTime = 0;
607634
return @{ @"error": @"Failed to start profiling" };
608635
}
609636
#else
@@ -615,6 +642,17 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
615642
{
616643
#if SENTRY_PROFILING_ENABLED
617644
try {
645+
NSDictionary<NSString *, id> * nativeProfile = nil;
646+
if (nativeProfileTraceId != nil && nativeProfileStartTime != 0) {
647+
#if SENTRY_TARGET_PROFILING_SUPPORTED
648+
uint64_t nativeProfileStopTime = [[[SentryDependencyContainer sharedInstance] dateProvider] systemTime];
649+
nativeProfile = [PrivateSentrySDKOnly collectProfileBetween:nativeProfileStartTime and:nativeProfileStopTime forTrace:nativeProfileTraceId];
650+
#endif
651+
}
652+
// Cleanup native profiles
653+
nativeProfileTraceId = nil;
654+
nativeProfileStartTime = 0;
655+
618656
facebook::hermes::HermesRuntime::disableSamplingProfiler();
619657
std::stringstream ss;
620658
facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);
@@ -633,10 +671,35 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
633671
}
634672
#endif
635673

636-
return @{ @"profile": data };
674+
if (data == nil) {
675+
return @{ @"error": @"Failed to retrieve Hermes profile." };
676+
}
677+
678+
if (nativeProfile == nil) {
679+
return @{ @"profile": data };
680+
}
681+
682+
return @{
683+
@"profile": data,
684+
@"nativeProfile": nativeProfile,
685+
};
637686
} catch (const std::exception& ex) {
687+
if (nativeProfileTraceId != nil) {
688+
#if SENTRY_TARGET_PROFILING_SUPPORTED
689+
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
690+
#endif
691+
nativeProfileTraceId = nil;
692+
}
693+
nativeProfileStartTime = 0;
638694
return @{ @"error": [NSString stringWithCString: ex.what() encoding:[NSString defaultCStringEncoding]] };
639695
} catch (...) {
696+
if (nativeProfileTraceId != nil) {
697+
#if SENTRY_TARGET_PROFILING_SUPPORTED
698+
[PrivateSentrySDKOnly discardProfilerForTrace: nativeProfileTraceId];
699+
#endif
700+
nativeProfileTraceId = nil;
701+
}
702+
nativeProfileStartTime = 0;
640703
return @{ @"error": @"Failed to stop profiling" };
641704
}
642705
#else

src/js/NativeRNSentry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface Spec extends TurboModule {
3232
fetchModules(): Promise<string | undefined | null>;
3333
fetchViewHierarchy(): Promise<number[] | undefined | null>;
3434
startProfiling(): { started?: boolean; error?: string };
35-
stopProfiling(): { profile?: string; error?: string };
35+
stopProfiling(): { profile?: string; nativeProfile?: UnsafeObject; error?: string };
3636
fetchNativePackageName(): Promise<string | undefined | null>;
3737
fetchNativeStackFramesBy(instructionsAddr: number[]): Promise<NativeStackFrames | undefined | null>;
3838
}

src/js/profiling/cache.ts

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

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

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

src/js/profiling/debugid.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { DebugImage } from '@sentry/types';
2+
import { GLOBAL_OBJ, logger } from '@sentry/utils';
3+
4+
import { DEFAULT_BUNDLE_NAME } from './hermes';
5+
6+
/**
7+
* Returns debug meta images of the loaded bundle.
8+
*/
9+
export function getDebugMetadata(): DebugImage[] {
10+
if (!DEFAULT_BUNDLE_NAME) {
11+
return [];
12+
}
13+
14+
const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
15+
if (!debugIdMap) {
16+
return [];
17+
}
18+
19+
const debugIdsKeys = Object.keys(debugIdMap);
20+
if (!debugIdsKeys.length) {
21+
return [];
22+
}
23+
24+
if (debugIdsKeys.length > 1) {
25+
logger.warn(
26+
'[Profiling] Multiple debug images found, but only one one bundle is supported. Using the first one...',
27+
);
28+
}
29+
30+
return [
31+
{
32+
code_file: DEFAULT_BUNDLE_NAME,
33+
debug_id: debugIdMap[debugIdsKeys[0]],
34+
type: 'sourcemap',
35+
},
36+
];
37+
}

src/js/profiling/hermes.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { Platform } from 'react-native';
22

33
import { ANDROID_DEFAULT_BUNDLE_NAME, IOS_DEFAULT_BUNDLE_NAME } from '../integrations/rewriteframes';
4-
import { NATIVE } from '../wrapper';
5-
import { convertToSentryProfile } from './convertHermesProfile';
6-
import type { RawThreadCpuProfile } from './types';
74

85
export type StackFrameId = number;
96
export type MicrosecondsSinceBoot = string;
@@ -55,29 +52,3 @@ export interface Profile {
5552

5653
export const DEFAULT_BUNDLE_NAME =
5754
Platform.OS === 'android' ? ANDROID_DEFAULT_BUNDLE_NAME : Platform.OS === 'ios' ? IOS_DEFAULT_BUNDLE_NAME : undefined;
58-
59-
const MS_TO_NS: number = 1e6;
60-
61-
/**
62-
* Starts Hermes Sampling Profiler and returns the timestamp when profiling started in nanoseconds.
63-
*/
64-
export function startProfiling(): number | null {
65-
const started = NATIVE.startProfiling();
66-
if (!started) {
67-
return null;
68-
}
69-
70-
const profileStartTimestampNs = Date.now() * MS_TO_NS;
71-
return profileStartTimestampNs;
72-
}
73-
74-
/**
75-
* Stops Hermes Sampling Profiler and returns the profile.
76-
*/
77-
export function stopProfiling(): RawThreadCpuProfile | null {
78-
const hermesProfile = NATIVE.stopProfiling();
79-
if (!hermesProfile) {
80-
return null;
81-
}
82-
return convertToSentryProfile(hermesProfile);
83-
}

0 commit comments

Comments
 (0)