Skip to content

Commit e43f1a2

Browse files
Profiling - Deduplication and cleanup (#4681)
* add readme and info about commit of the source repository * delete jfr file on jvm exit * further split into smaller methods * deduplicate frames in order to save bandwidth, add converter tests * remove Platform Enum, use string constants instead for compatibility with cross platform frameworks * implement equals and hashcode for SentryStackFrame to make frame deduplication work * bump api * improve error handling, fix start stop start flow * add new testfile * calculate ticksPerNanosecond in constructor * adapt Ratelimiter to check for both ProfileChunk and ProfileChunkUi ratelimiting * update ratelimiter test to check for both profileChunk and profileChunkUi drops * use string constant instead of string * Format code * add non aggregating event collector to send each event individually, deduplicate stacks * adapt converter tests to new non-aggregated converter * Format code * add logging to loadProfileConverter * Format code * fix duplication of events * catch all exception happening when converting from jfr * add exists and writable info to log message * add method to safely delete file * remove setNative call * fix test * fix reference to commit we vendored from * drop event if it cannot be processed to not lose the whole chunk * make format * fix test * Format code * Profiling - OTEL profiling fix, Stabilization, Logging (#4746) * add skipProfiling flag to TransactionOptions to be able to skip profiling and handle cases where profiling has been started by otel * add profilerId to spanContext so that otel span processor can propagate this to the exporter and SentryTracer * immediately end profiling when stopProfiler is called * bump api, fix android api 24 code * catch all exception happening when converting from jfr * simplify JavaContinuous profiler by catching AsyncProfiler instantiation exceptions in provider * add exists and writable info to log message * add method to safely delete file * remove setNative call * fix test * fix reference to commit we vendored from * drop event if it cannot be processed to not lose the whole chunk * Format code * fix test * Format code * fix test * catch exceptions in startProfiler/stopProfiler * fallback to threadId -1 if it cannot be resolved --------- Co-authored-by: Sentry Github Bot <[email protected]> --------- Co-authored-by: Sentry Github Bot <[email protected]>
1 parent 8b7e489 commit e43f1a2

File tree

25 files changed

+702
-225
lines changed

25 files changed

+702
-225
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ private void stop(final boolean restartProfiler) {
301301
endData.measurementsMap,
302302
endData.traceFile,
303303
startProfileChunkTimestamp,
304-
ProfileChunk.Platform.ANDROID));
304+
ProfileChunk.PLATFORM_ANDROID));
305305
}
306306
}
307307

sentry-async-profiler/api/sentry-async-profiler.api

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ public final class io/sentry/asyncprofiler/BuildConfig {
44
}
55

66
public final class io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter {
7-
public fun <init> (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;Lio/sentry/SentryStackTraceFactory;)V
7+
public fun <init> (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;Lio/sentry/SentryStackTraceFactory;Lio/sentry/ILogger;)V
88
public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile;
99
}
1010

11+
public final class io/sentry/asyncprofiler/convert/NonAggregatingEventCollector : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector {
12+
public fun <init> ()V
13+
public fun afterChunk ()V
14+
public fun beforeChunk ()V
15+
public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V
16+
public fun finish ()Z
17+
public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V
18+
}
19+
1120
public final class io/sentry/asyncprofiler/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
1221
public fun <init> (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V
1322
public fun close (Z)V

sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,88 @@
11
package io.sentry.asyncprofiler.convert;
22

33
import io.sentry.DateUtils;
4+
import io.sentry.ILogger;
45
import io.sentry.Sentry;
6+
import io.sentry.SentryLevel;
57
import io.sentry.SentryStackTraceFactory;
68
import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments;
79
import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.JfrConverter;
810
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader;
911
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace;
1012
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event;
13+
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector;
1114
import io.sentry.protocol.SentryStackFrame;
1215
import io.sentry.protocol.profiling.SentryProfile;
1316
import io.sentry.protocol.profiling.SentrySample;
1417
import io.sentry.protocol.profiling.SentryThreadMetadata;
1518
import java.io.IOException;
1619
import java.nio.file.Path;
1720
import java.util.ArrayList;
21+
import java.util.HashMap;
1822
import java.util.List;
23+
import java.util.Map;
1924
import org.jetbrains.annotations.NotNull;
2025
import org.jetbrains.annotations.Nullable;
2126

2227
public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter {
23-
private static final long NANOS_PER_SECOND = 1_000_000_000L;
28+
private static final double NANOS_PER_SECOND = 1_000_000_000.0;
29+
private static final long UNKNOWN_THREAD_ID = -1;
2430

2531
private final @NotNull SentryProfile sentryProfile = new SentryProfile();
2632
private final @NotNull SentryStackTraceFactory stackTraceFactory;
33+
private final @NotNull ILogger logger;
34+
private final @NotNull Map<SentryStackFrame, Integer> frameDeduplicationMap = new HashMap<>();
35+
private final @NotNull Map<List<Integer>, Integer> stackDeduplicationMap = new HashMap<>();
2736

2837
public JfrAsyncProfilerToSentryProfileConverter(
29-
JfrReader jfr, Arguments args, @NotNull SentryStackTraceFactory stackTraceFactory) {
38+
JfrReader jfr,
39+
Arguments args,
40+
@NotNull SentryStackTraceFactory stackTraceFactory,
41+
@NotNull ILogger logger) {
3042
super(jfr, args);
3143
this.stackTraceFactory = stackTraceFactory;
44+
this.logger = logger;
3245
}
3346

3447
@Override
3548
protected void convertChunk() {
3649
collector.forEach(new ProfileEventVisitor(sentryProfile, stackTraceFactory, jfr, args));
3750
}
3851

52+
@Override
53+
protected EventCollector createCollector(Arguments args) {
54+
return new NonAggregatingEventCollector();
55+
}
56+
3957
public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath)
4058
throws IOException {
4159
JfrAsyncProfilerToSentryProfileConverter converter;
4260
try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) {
4361
Arguments args = new Arguments();
4462
args.cpu = false;
63+
args.wall = true;
4564
args.alloc = false;
4665
args.threads = true;
4766
args.lines = true;
4867
args.dot = true;
4968

5069
SentryStackTraceFactory stackTraceFactory =
5170
new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions());
52-
converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory);
71+
ILogger logger = Sentry.getGlobalScope().getOptions().getLogger();
72+
converter =
73+
new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory, logger);
5374
converter.convert();
5475
}
5576

5677
return converter.sentryProfile;
5778
}
5879

59-
private class ProfileEventVisitor extends AggregatedEventVisitor {
80+
private class ProfileEventVisitor implements EventCollector.Visitor {
6081
private final @NotNull SentryProfile sentryProfile;
6182
private final @NotNull SentryStackTraceFactory stackTraceFactory;
6283
private final @NotNull JfrReader jfr;
6384
private final @NotNull Arguments args;
85+
private final double ticksPerNanosecond;
6486

6587
public ProfileEventVisitor(
6688
@NotNull SentryProfile sentryProfile,
@@ -71,25 +93,37 @@ public ProfileEventVisitor(
7193
this.stackTraceFactory = stackTraceFactory;
7294
this.jfr = jfr;
7395
this.args = args;
96+
ticksPerNanosecond = jfr.ticksPerSec / NANOS_PER_SECOND;
7497
}
7598

7699
@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-
}
100+
public void visit(Event event, long samples, long value) {
101+
try {
102+
StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId);
103+
long threadId = resolveThreadId(event.tid);
85104

86-
createSample(event, threadId);
105+
if (stackTrace != null) {
106+
if (args.threads) {
107+
processThreadMetadata(event, threadId);
108+
}
87109

88-
buildStackTraceAndFrames(stackTrace);
110+
processSampleWithStack(event, threadId, stackTrace);
111+
}
112+
} catch (Exception e) {
113+
logger.log(SentryLevel.WARNING, "Failed to process JFR event " + event, e);
89114
}
90115
}
91116

117+
private long resolveThreadId(int eventId) {
118+
Long javaThreadId = jfr.javaThreads.get(eventId);
119+
return javaThreadId != null ? javaThreadId : UNKNOWN_THREAD_ID;
120+
}
121+
92122
private void processThreadMetadata(Event event, long threadId) {
123+
if (threadId == UNKNOWN_THREAD_ID) {
124+
return;
125+
}
126+
93127
final String threadName = getPlainThreadName(event.tid);
94128
sentryProfile
95129
.getThreadMetadata()
@@ -103,28 +137,72 @@ private void processThreadMetadata(Event event, long threadId) {
103137
});
104138
}
105139

106-
private void buildStackTraceAndFrames(StackTrace stackTrace) {
107-
List<Integer> stack = new ArrayList<>();
108-
int currentFrame = sentryProfile.getFrames().size();
140+
private void processSampleWithStack(Event event, long threadId, StackTrace stackTrace) {
141+
int stackIndex = addStackTrace(stackTrace);
142+
143+
SentrySample sample = new SentrySample();
144+
sample.setTimestamp(calculateTimestamp(event));
145+
sample.setThreadId(String.valueOf(threadId));
146+
sample.setStackId(stackIndex);
147+
148+
sentryProfile.getSamples().add(sample);
149+
}
150+
151+
private double calculateTimestamp(Event event) {
152+
long nanosFromStart = (long) ((event.time - jfr.chunkStartTicks) / ticksPerNanosecond);
153+
154+
long timeNs = jfr.chunkStartNanos + nanosFromStart;
155+
156+
return DateUtils.nanosToSeconds(timeNs);
157+
}
158+
159+
private int addStackTrace(StackTrace stackTrace) {
160+
List<Integer> callStack = createFramesAndCallStack(stackTrace);
161+
162+
Integer existingIndex = stackDeduplicationMap.get(callStack);
163+
if (existingIndex != null) {
164+
return existingIndex;
165+
}
166+
167+
int stackIndex = sentryProfile.getStacks().size();
168+
sentryProfile.getStacks().add(callStack);
169+
stackDeduplicationMap.put(callStack, stackIndex);
170+
return stackIndex;
171+
}
172+
173+
private List<Integer> createFramesAndCallStack(StackTrace stackTrace) {
174+
List<Integer> callStack = new ArrayList<>();
109175

110176
long[] methods = stackTrace.methods;
111177
byte[] types = stackTrace.types;
112178
int[] locations = stackTrace.locations;
113179

114180
for (int i = 0; i < methods.length; i++) {
115181
StackTraceElement element = getStackTraceElement(methods[i], types[i], locations[i]);
116-
if (element.isNativeMethod()) {
182+
if (element.isNativeMethod() || isNativeFrame(types[i])) {
117183
continue;
118184
}
119185

120186
SentryStackFrame frame = createStackFrame(element);
121-
sentryProfile.getFrames().add(frame);
187+
int frameIndex = getOrAddFrame(frame);
188+
callStack.add(frameIndex);
189+
}
190+
191+
return callStack;
192+
}
193+
194+
// Get existing frame index or add new frame and return its index
195+
private int getOrAddFrame(SentryStackFrame frame) {
196+
Integer existingIndex = frameDeduplicationMap.get(frame);
122197

123-
stack.add(currentFrame);
124-
currentFrame++;
198+
if (existingIndex != null) {
199+
return existingIndex;
125200
}
126201

127-
sentryProfile.getStacks().add(stack);
202+
int newIndex = sentryProfile.getFrames().size();
203+
sentryProfile.getFrames().add(frame);
204+
frameDeduplicationMap.put(frame, newIndex);
205+
return newIndex;
128206
}
129207

130208
private SentryStackFrame createStackFrame(StackTraceElement element) {
@@ -176,36 +254,12 @@ private boolean isRegularClassWithoutPackage(String className) {
176254
return !className.startsWith("[");
177255
}
178256

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-
197257
private boolean shouldMarkAsSystemFrame(StackTraceElement element, String className) {
198258
return element.isNativeMethod() || className.isEmpty();
199259
}
200260

201261
private @Nullable Integer extractLineNumber(StackTraceElement element) {
202262
return element.getLineNumber() != 0 ? element.getLineNumber() : null;
203263
}
204-
205-
private long resolveThreadId(int eventThreadId) {
206-
return jfr.threads.get(eventThreadId) != null
207-
? jfr.javaThreads.get(eventThreadId)
208-
: eventThreadId;
209-
}
210264
}
211265
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.sentry.asyncprofiler.convert;
2+
3+
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event;
4+
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector;
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
public final class NonAggregatingEventCollector implements EventCollector {
9+
final List<Event> events = new ArrayList<>();
10+
11+
@Override
12+
public void collect(Event e) {
13+
events.add(e);
14+
}
15+
16+
@Override
17+
public void beforeChunk() {
18+
// No-op
19+
}
20+
21+
@Override
22+
public void afterChunk() {
23+
// No-op
24+
}
25+
26+
@Override
27+
public boolean finish() {
28+
return false;
29+
}
30+
31+
@Override
32+
public void forEach(Visitor visitor) {
33+
for (Event event : events) {
34+
visitor.visit(event, event.samples(), event.value());
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)