Skip to content

Commit 5446992

Browse files
fix(profiling): Parse Hermes Bytecode frames virtual address (#3342)
1 parent 21465bc commit 5446992

File tree

5 files changed

+107
-33
lines changed

5 files changed

+107
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- Add actual `activeThreadId` to Profiles ([#3338](https://github.com/getsentry/sentry-react-native/pull/3338))
8+
- Parse Hermes Profiling Bytecode Frames ([#3342](https://github.com/getsentry/sentry-react-native/pull/3342))
89

910
## 5.11.1
1011

src/js/profiling/convertHermesProfile.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import type { FrameId, StackId, ThreadCpuFrame, ThreadCpuSample, ThreadCpuStack, ThreadId } from '@sentry/types';
22
import { logger } from '@sentry/utils';
3-
import { Platform } from 'react-native';
43

5-
import { ANDROID_DEFAULT_BUNDLE_NAME, IOS_DEFAULT_BUNDLE_NAME } from '../integrations/rewriteframes';
64
import type * as Hermes from './hermes';
7-
import { parseHermesStackFrameFunctionName } from './hermes';
5+
import { parseHermesJSStackFrame } from './hermes';
86
import { MAX_PROFILE_DURATION_MS } from './integration';
97
import type { RawThreadCpuProfile } from './types';
108

119
const PLACEHOLDER_THREAD_ID_STRING = '0';
1210
const MS_TO_NS = 1e6;
1311
const MAX_PROFILE_DURATION_NS = MAX_PROFILE_DURATION_MS * MS_TO_NS;
14-
const ANONYMOUS_FUNCTION_NAME = 'anonymous';
1512
const UNKNOWN_STACK_ID = -1;
1613
const JS_THREAD_NAME = 'JavaScriptThread';
1714
const JS_THREAD_PRIORITY = 1;
18-
const DEFAULT_BUNDLE_NAME =
19-
Platform.OS === 'android' ? ANDROID_DEFAULT_BUNDLE_NAME : Platform.OS === 'ios' ? IOS_DEFAULT_BUNDLE_NAME : undefined;
2015

2116
/**
2217
* Converts a Hermes profile to a Sentry profile.
@@ -136,15 +131,7 @@ function mapFrames(hermesStackFrames: Record<Hermes.StackFrameId, Hermes.StackFr
136131
continue;
137132
}
138133
hermesStackFrameIdToSentryFrameIdMap.set(Number(key), frames.length);
139-
const hermesFrame = hermesStackFrames[key];
140-
141-
const functionName = parseHermesStackFrameFunctionName(hermesFrame.name);
142-
frames.push({
143-
function: functionName || ANONYMOUS_FUNCTION_NAME,
144-
file: hermesFrame.category == 'JavaScript' ? DEFAULT_BUNDLE_NAME : undefined,
145-
lineno: hermesFrame.line !== undefined ? Number(hermesFrame.line) : undefined,
146-
colno: hermesFrame.column !== undefined ? Number(hermesFrame.column) : undefined,
147-
});
134+
frames.push(parseHermesJSStackFrame(hermesStackFrames[key]));
148135
}
149136

150137
return {

src/js/profiling/hermes.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Platform } from 'react-native';
2+
3+
import { ANDROID_DEFAULT_BUNDLE_NAME, IOS_DEFAULT_BUNDLE_NAME } from '../integrations/rewriteframes';
14
import { NATIVE } from '../wrapper';
25
import { convertToSentryProfile } from './convertHermesProfile';
36
import type { RawThreadCpuProfile } from './types';
@@ -28,10 +31,17 @@ export interface Sample {
2831
}
2932

3033
export interface StackFrame {
34+
// Hermes Bytecode
35+
funcVirtAddr?: string;
36+
offset?: string;
37+
38+
// JavaScript
3139
line?: string;
3240
column?: string;
3341
funcLine?: string;
3442
funcColumn?: string;
43+
44+
// Common
3545
name: string;
3646
category: string;
3747
parent?: number;
@@ -43,15 +53,50 @@ export interface Profile {
4353
stackFrames: Record<string, StackFrame>;
4454
}
4555

56+
export interface ParsedHermesStackFrame {
57+
function: string;
58+
file?: string;
59+
lineno?: number;
60+
colno?: number;
61+
}
62+
63+
const DEFAULT_BUNDLE_NAME =
64+
Platform.OS === 'android' ? ANDROID_DEFAULT_BUNDLE_NAME : Platform.OS === 'ios' ? IOS_DEFAULT_BUNDLE_NAME : undefined;
65+
const ANONYMOUS_FUNCTION_NAME = 'anonymous';
66+
4667
/**
47-
* Hermes Profile Stack Frame Name contains function name and file path.
48-
*
49-
* `foo(/path/to/file.js:1:2)` -> `foo`
68+
* Parses Hermes StackFrame to Sentry StackFrame.
69+
* For native frames only function name is returned, for Hermes bytecode the line and column are calculated.
5070
*/
51-
export function parseHermesStackFrameFunctionName(hermesName: string): string {
52-
const indexOfLeftParenthesis = hermesName.indexOf('(');
53-
const name = indexOfLeftParenthesis !== -1 ? hermesName.substring(0, indexOfLeftParenthesis) : hermesName;
54-
return name;
71+
export function parseHermesJSStackFrame(frame: StackFrame): ParsedHermesStackFrame {
72+
if (frame.category !== 'JavaScript') {
73+
// Native
74+
return { function: frame.name };
75+
}
76+
77+
if (frame.funcVirtAddr !== undefined && frame.offset !== undefined) {
78+
// Hermes Bytecode
79+
return {
80+
function: frame.name || ANONYMOUS_FUNCTION_NAME,
81+
file: DEFAULT_BUNDLE_NAME,
82+
// https://github.com/krystofwoldrich/metro/blob/417e6f276ff9422af6039fc4d1bce41fcf7d9f46/packages/metro-symbolicate/src/Symbolication.js#L298-L301
83+
// Hermes lineno is hardcoded 1, currently only one bundle symbolication is supported by metro-symbolicate and thus by us.
84+
lineno: 1,
85+
// Hermes colno is 0-based, while Sentry is 1-based
86+
colno: Number(frame.funcVirtAddr) + Number(frame.offset) + 1,
87+
};
88+
}
89+
90+
// JavaScript
91+
const indexOfLeftParenthesis = frame.name.indexOf('(');
92+
return {
93+
function:
94+
(indexOfLeftParenthesis !== -1 && (frame.name.substring(0, indexOfLeftParenthesis) || ANONYMOUS_FUNCTION_NAME)) ||
95+
frame.name,
96+
file: DEFAULT_BUNDLE_NAME,
97+
lineno: frame.line !== undefined ? Number(frame.line) : undefined,
98+
colno: frame.column !== undefined ? Number(frame.column) : undefined,
99+
};
55100
}
56101

57102
const MS_TO_NS: number = 1e6;

test/profiling/convertHermesProfile.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('convert hermes profile to sentry profile', () => {
5656
column: '33',
5757
funcLine: '1605',
5858
funcColumn: '14',
59-
name: 'fooA(/absolute/path/app:///main.jsbundle:1610:33)',
59+
name: 'fooA(app:///main.jsbundle:1610:33)',
6060
category: 'JavaScript',
6161
parent: 1,
6262
},
@@ -65,7 +65,7 @@ describe('convert hermes profile to sentry profile', () => {
6565
column: '21',
6666
funcLine: '1614',
6767
funcColumn: '14',
68-
name: 'fooB(/absolute/path/app:///main.jsbundle:1616:21)',
68+
name: 'fooB(http://localhost:8081/index.bundle//&platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=org.reactjs.native.example.sampleNewArchitecture:193430:4)',
6969
category: 'JavaScript',
7070
parent: 1,
7171
},
@@ -74,7 +74,7 @@ describe('convert hermes profile to sentry profile', () => {
7474
column: '18',
7575
funcLine: '1623',
7676
funcColumn: '16',
77-
name: '(/absolute/path/app:///main.jsbundle:1627:18)',
77+
name: '(/Users/distiller/react-native/packages/react-native/sdks/hermes/build_iphonesimulator/lib/InternalBytecode/InternalBytecode.js:139:27)',
7878
category: 'JavaScript',
7979
parent: 2,
8080
},
@@ -83,10 +83,7 @@ describe('convert hermes profile to sentry profile', () => {
8383
const expectedSentryProfile: RawThreadCpuProfile = {
8484
frames: [
8585
{
86-
colno: undefined,
87-
file: undefined,
8886
function: '[root]',
89-
lineno: undefined,
9087
},
9188
{
9289
colno: 33,

test/profiling/hermes.test.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,62 @@
1-
import { parseHermesStackFrameFunctionName } from '../../src/js/profiling/hermes';
1+
import type { ParsedHermesStackFrame } from '../../src/js/profiling/hermes';
2+
import { parseHermesJSStackFrame } from '../../src/js/profiling/hermes';
23

34
describe('hermes', () => {
45
describe('parseHermesStackFrameName', () => {
56
test('parses function name and file name', () => {
6-
expect(parseHermesStackFrameFunctionName('fooA(/absolute/path/main.jsbundle:1610:33)')).toEqual('fooA');
7+
expect(
8+
parseHermesJSStackFrame({
9+
name: 'fooA(/absolute/path/main.jsbundle:1610:33)',
10+
line: '1610',
11+
column: '33',
12+
category: 'JavaScript',
13+
}),
14+
).toEqual(<ParsedHermesStackFrame>{
15+
function: 'fooA',
16+
file: 'app:///main.jsbundle',
17+
lineno: 1610,
18+
colno: 33,
19+
});
720
});
821
test('parse hermes root stack frame', () => {
9-
expect(parseHermesStackFrameFunctionName('[root]')).toEqual('[root]');
22+
expect(
23+
parseHermesJSStackFrame({
24+
name: '[root]',
25+
category: 'root',
26+
}),
27+
).toEqual(
28+
expect.objectContaining(<ParsedHermesStackFrame>{
29+
function: '[root]',
30+
}),
31+
);
1032
});
1133
test('parse only file name', () => {
12-
expect(parseHermesStackFrameFunctionName('(/absolute/path/jsbundle:1610:33)')).toEqual('');
34+
expect(
35+
parseHermesJSStackFrame({
36+
name: '(/absolute/path/main.jsbundle:1610:33)',
37+
line: '1610',
38+
column: '33',
39+
category: 'JavaScript',
40+
}),
41+
).toEqual(<ParsedHermesStackFrame>{
42+
function: 'anonymous',
43+
file: 'app:///main.jsbundle',
44+
lineno: 1610,
45+
colno: 33,
46+
});
1347
});
1448
test('parse only function name', () => {
15-
expect(parseHermesStackFrameFunctionName('fooA')).toEqual('fooA');
49+
expect(
50+
parseHermesJSStackFrame({
51+
name: 'fooA',
52+
category: 'JavaScript',
53+
}),
54+
).toEqual(
55+
expect.objectContaining(<ParsedHermesStackFrame>{
56+
function: 'fooA',
57+
file: 'app:///main.jsbundle',
58+
}),
59+
);
1660
});
1761
});
1862
});

0 commit comments

Comments
 (0)