11package io .sentry .asyncprofiler .convert ;
22
3+ import io .sentry .DateUtils ;
34import io .sentry .Sentry ;
45import io .sentry .SentryStackTraceFactory ;
56import io .sentry .asyncprofiler .vendor .asyncprofiler .convert .Arguments ;
89import io .sentry .asyncprofiler .vendor .asyncprofiler .jfr .StackTrace ;
910import io .sentry .asyncprofiler .vendor .asyncprofiler .jfr .event .Event ;
1011import io .sentry .protocol .SentryStackFrame ;
11- import io .sentry .protocol .profiling .JfrSample ;
1212import io .sentry .protocol .profiling .SentryProfile ;
13- import io .sentry .protocol .profiling .ThreadMetadata ;
13+ import io .sentry .protocol .profiling .SentrySample ;
14+ import io .sentry .protocol .profiling .SentryThreadMetadata ;
1415import java .io .IOException ;
1516import java .nio .file .Path ;
16- import java .time .Instant ;
1717import java .util .ArrayList ;
18- import java .util .HashMap ;
1918import java .util .List ;
2019import org .jetbrains .annotations .NotNull ;
20+ import org .jetbrains .annotations .Nullable ;
2121
2222public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter {
23+ private static final long NANOS_PER_SECOND = 1_000_000_000L ;
24+
2325 private final @ NotNull SentryProfile sentryProfile = new SentryProfile ();
26+ private final @ NotNull SentryStackTraceFactory stackTraceFactory ;
2427
25- public JfrAsyncProfilerToSentryProfileConverter (JfrReader jfr , Arguments args ) {
28+ public JfrAsyncProfilerToSentryProfileConverter (
29+ JfrReader jfr , Arguments args , @ NotNull SentryStackTraceFactory stackTraceFactory ) {
2630 super (jfr , args );
31+ this .stackTraceFactory = stackTraceFactory ;
2732 }
2833
2934 @ Override
3035 protected void convertChunk () {
31- final List <Event > events = new ArrayList <Event >();
32- final List <List <Integer >> stacks = new ArrayList <>();
33-
34- collector .forEach (
35- new AggregatedEventVisitor () {
36-
37- @ Override
38- public void visit (Event event , long value ) {
39- events .add (event );
40- System .out .println (event );
41- StackTrace stackTrace = jfr .stackTraces .get (event .stackTraceId );
42-
43- if (stackTrace != null ) {
44- Arguments args = JfrAsyncProfilerToSentryProfileConverter .this .args ;
45- long [] methods = stackTrace .methods ;
46- byte [] types = stackTrace .types ;
47- int [] locations = stackTrace .locations ;
48-
49- if (args .threads ) {
50- if (sentryProfile .threadMetadata == null ) {
51- sentryProfile .threadMetadata = new HashMap <>();
52- }
53-
54- long threadIdToUse =
55- jfr .threads .get (event .tid ) != null ? jfr .javaThreads .get (event .tid ) : event .tid ;
56-
57- if (sentryProfile .threadMetadata != null ) {
58- final String threadName = getPlainThreadName (event .tid );
59- sentryProfile .threadMetadata .computeIfAbsent (
60- String .valueOf (threadIdToUse ),
61- k -> {
62- ThreadMetadata metadata = new ThreadMetadata ();
63- metadata .name = threadName ;
64- metadata .priority = 0 ;
65- return metadata ;
66- });
67- }
68- }
69-
70- if (sentryProfile .samples == null ) {
71- sentryProfile .samples = new ArrayList <>();
72- }
73-
74- if (sentryProfile .frames == null ) {
75- sentryProfile .frames = new ArrayList <>();
76- }
77-
78- List <Integer > stack = new ArrayList <>();
79- int currentStack = stacks .size ();
80- int currentFrame = sentryProfile .frames != null ? sentryProfile .frames .size () : 0 ;
81- for (int i = 0 ; i < methods .length ; i ++) {
82- // for (int i = methods.length; --i >= 0; ) {
83- SentryStackFrame frame = new SentryStackFrame ();
84- StackTraceElement element =
85- getStackTraceElement (methods [i ], types [i ], locations [i ]);
86- if (element .isNativeMethod ()) {
87- continue ;
88- }
89-
90- final String classNameWithLambdas = element .getClassName ().replace ("/" , "." );
91- frame .setFunction (element .getMethodName ());
92-
93- int firstDollar = classNameWithLambdas .indexOf ('$' );
94- String sanitizedClassName = classNameWithLambdas ;
95- if (firstDollar != -1 ) {
96- sanitizedClassName = classNameWithLambdas .substring (0 , firstDollar );
97- }
98-
99- int lastDot = sanitizedClassName .lastIndexOf ('.' );
100- if (lastDot > 0 ) {
101- frame .setModule (sanitizedClassName );
102- } else if (!classNameWithLambdas .startsWith ("[" )) {
103- frame .setModule ("" );
104- }
105-
106- if (element .isNativeMethod () || classNameWithLambdas .isEmpty ()) {
107- frame .setInApp (false );
108- } else {
109- frame .setInApp (
110- new SentryStackTraceFactory (Sentry .getGlobalScope ().getOptions ())
111- .isInApp (sanitizedClassName ));
112- }
113-
114- frame .setLineno ((element .getLineNumber () != 0 ) ? element .getLineNumber () : null );
115- frame .setFilename (classNameWithLambdas );
116-
117- if (sentryProfile .frames != null ) {
118- sentryProfile .frames .add (frame );
119- }
120- stack .add (currentFrame );
121- currentFrame ++;
122- }
123-
124- long divisor = jfr .ticksPerSec / 1000_000_000L ;
125- long myTimeStamp =
126- jfr .chunkStartNanos + ((event .time - jfr .chunkStartTicks ) / divisor );
127-
128- JfrSample sample = new JfrSample ();
129- Instant instant = Instant .ofEpochSecond (0 , myTimeStamp );
130- double timestampDouble =
131- instant .getEpochSecond () + instant .getNano () / 1_000_000_000.0 ;
132-
133- sample .timestamp = timestampDouble ;
134- sample .threadId =
135- String .valueOf (
136- jfr .threads .get (event .tid ) != null
137- ? jfr .javaThreads .get (event .tid )
138- : event .tid );
139- sample .stackId = currentStack ;
140- if (sentryProfile .samples != null ) {
141- sentryProfile .samples .add (sample );
142- }
143-
144- stacks .add (stack );
145- }
146- }
147- });
148- sentryProfile .stacks = stacks ;
149- System .out .println ("Samples: " + events .size ());
36+ collector .forEach (new ProfileEventVisitor (sentryProfile , stackTraceFactory , jfr , args ));
15037 }
15138
15239 public static @ NotNull SentryProfile convertFromFileStatic (@ NotNull Path jfrFilePath )
@@ -160,10 +47,165 @@ public void visit(Event event, long value) {
16047 args .lines = true ;
16148 args .dot = true ;
16249
163- converter = new JfrAsyncProfilerToSentryProfileConverter (jfrReader , args );
50+ SentryStackTraceFactory stackTraceFactory =
51+ new SentryStackTraceFactory (Sentry .getGlobalScope ().getOptions ());
52+ converter = new JfrAsyncProfilerToSentryProfileConverter (jfrReader , args , stackTraceFactory );
16453 converter .convert ();
16554 }
16655
16756 return converter .sentryProfile ;
16857 }
58+
59+ private class ProfileEventVisitor extends AggregatedEventVisitor {
60+ private final @ NotNull SentryProfile sentryProfile ;
61+ private final @ NotNull SentryStackTraceFactory stackTraceFactory ;
62+ private final @ NotNull JfrReader jfr ;
63+ private final @ NotNull Arguments args ;
64+
65+ public ProfileEventVisitor (
66+ @ NotNull SentryProfile sentryProfile ,
67+ @ NotNull SentryStackTraceFactory stackTraceFactory ,
68+ @ NotNull JfrReader jfr ,
69+ @ NotNull Arguments args ) {
70+ this .sentryProfile = sentryProfile ;
71+ this .stackTraceFactory = stackTraceFactory ;
72+ this .jfr = jfr ;
73+ this .args = args ;
74+ }
75+
76+ @ Override
77+ public void visit (Event event , long value ) {
78+ StackTrace stackTrace = jfr .stackTraces .get (event .stackTraceId );
79+ long threadId = resolveThreadId (event .tid );
80+
81+ if (stackTrace != null ) {
82+ if (args .threads ) {
83+ processThreadMetadata (event , threadId );
84+ }
85+
86+ createSample (event , threadId );
87+
88+ buildStackTraceAndFrames (stackTrace );
89+ }
90+ }
91+
92+ private void processThreadMetadata (Event event , long threadId ) {
93+ final String threadName = getPlainThreadName (event .tid );
94+ sentryProfile
95+ .getThreadMetadata ()
96+ .computeIfAbsent (
97+ String .valueOf (threadId ),
98+ k -> {
99+ SentryThreadMetadata metadata = new SentryThreadMetadata ();
100+ metadata .setName (threadName );
101+ metadata .setPriority (0 ); // Default priority
102+ return metadata ;
103+ });
104+ }
105+
106+ private void buildStackTraceAndFrames (StackTrace stackTrace ) {
107+ List <Integer > stack = new ArrayList <>();
108+ int currentFrame = sentryProfile .getFrames ().size ();
109+
110+ long [] methods = stackTrace .methods ;
111+ byte [] types = stackTrace .types ;
112+ int [] locations = stackTrace .locations ;
113+
114+ for (int i = 0 ; i < methods .length ; i ++) {
115+ StackTraceElement element = getStackTraceElement (methods [i ], types [i ], locations [i ]);
116+ if (element .isNativeMethod ()) {
117+ continue ;
118+ }
119+
120+ SentryStackFrame frame = createStackFrame (element );
121+ sentryProfile .getFrames ().add (frame );
122+
123+ stack .add (currentFrame );
124+ currentFrame ++;
125+ }
126+
127+ sentryProfile .getStacks ().add (stack );
128+ }
129+
130+ private SentryStackFrame createStackFrame (StackTraceElement element ) {
131+ SentryStackFrame frame = new SentryStackFrame ();
132+ final String classNameWithLambdas = element .getClassName ().replace ("/" , "." );
133+ frame .setFunction (element .getMethodName ());
134+
135+ String sanitizedClassName = extractSanitizedClassName (classNameWithLambdas );
136+ frame .setModule (extractModuleName (sanitizedClassName , classNameWithLambdas ));
137+
138+ if (shouldMarkAsSystemFrame (element , classNameWithLambdas )) {
139+ frame .setInApp (false );
140+ } else {
141+ frame .setInApp (stackTraceFactory .isInApp (sanitizedClassName ));
142+ }
143+
144+ frame .setLineno (extractLineNumber (element ));
145+ frame .setFilename (classNameWithLambdas );
146+
147+ return frame ;
148+ }
149+
150+ // Remove lambda suffix from class name
151+ private String extractSanitizedClassName (String classNameWithLambdas ) {
152+ int firstDollar = classNameWithLambdas .indexOf ('$' );
153+ if (firstDollar != -1 ) {
154+ return classNameWithLambdas .substring (0 , firstDollar );
155+ }
156+ return classNameWithLambdas ;
157+ }
158+
159+ // TODO: test difference between null and empty string for module
160+ private @ Nullable String extractModuleName (
161+ String sanitizedClassName , String classNameWithLambdas ) {
162+ if (hasPackageStructure (sanitizedClassName )) {
163+ return sanitizedClassName ;
164+ } else if (isRegularClassWithoutPackage (classNameWithLambdas )) {
165+ return "" ;
166+ } else {
167+ return null ;
168+ }
169+ }
170+
171+ private boolean hasPackageStructure (String className ) {
172+ return className .lastIndexOf ('.' ) > 0 ;
173+ }
174+
175+ private boolean isRegularClassWithoutPackage (String className ) {
176+ return !className .startsWith ("[" );
177+ }
178+
179+ private void createSample (Event event , long threadId ) {
180+ int stackId = sentryProfile .getStacks ().size ();
181+ SentrySample sample = new SentrySample ();
182+
183+ // Calculate timestamp from JFR event time
184+ long nsFromStart =
185+ (event .time - jfr .chunkStartTicks )
186+ * JfrAsyncProfilerToSentryProfileConverter .NANOS_PER_SECOND
187+ / jfr .ticksPerSec ;
188+ long timeNs = jfr .chunkStartNanos + nsFromStart ;
189+ sample .setTimestamp (DateUtils .nanosToSeconds (timeNs ));
190+
191+ sample .setThreadId (String .valueOf (threadId ));
192+ sample .setStackId (stackId );
193+
194+ sentryProfile .getSamples ().add (sample );
195+ }
196+
197+ private boolean shouldMarkAsSystemFrame (StackTraceElement element , String className ) {
198+ return element .isNativeMethod () || className .isEmpty ();
199+ }
200+
201+ private @ Nullable Integer extractLineNumber (StackTraceElement element ) {
202+ return element .getLineNumber () != 0 ? element .getLineNumber () : null ;
203+ }
204+
205+ private long resolveThreadId (int eventThreadId ) {
206+ return jfr .threads .get (eventThreadId ) != null
207+ ? jfr .javaThreads .get (eventThreadId )
208+ : eventThreadId ;
209+ }
210+ }
169211}
0 commit comments