Skip to content
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb12990
add readme and info about commit of the source repository
lbloder Aug 4, 2025
14d2f3b
delete jfr file on jvm exit
lbloder Aug 5, 2025
bdd7b3a
further split into smaller methods
lbloder Aug 5, 2025
1b88a3d
deduplicate frames in order to save bandwidth, add converter tests
lbloder Aug 22, 2025
81aef38
remove Platform Enum, use string constants instead for compatibility …
lbloder Aug 25, 2025
8f32429
implement equals and hashcode for SentryStackFrame to make frame dedu…
lbloder Aug 25, 2025
b7645f2
bump api
lbloder Aug 25, 2025
66ae530
improve error handling, fix start stop start flow
lbloder Aug 26, 2025
037b237
add new testfile
lbloder Aug 26, 2025
2979b16
calculate ticksPerNanosecond in constructor
lbloder Aug 26, 2025
a32b71e
adapt Ratelimiter to check for both ProfileChunk and ProfileChunkUi r…
lbloder Aug 29, 2025
4b1e57e
update ratelimiter test to check for both profileChunk and profileChu…
lbloder Aug 29, 2025
8992018
use string constant instead of string
lbloder Aug 29, 2025
c9a6bc0
Format code
getsentry-bot Aug 29, 2025
451f191
add non aggregating event collector to send each event individually, …
lbloder Sep 4, 2025
885f0ab
adapt converter tests to new non-aggregated converter
lbloder Sep 5, 2025
7ae393e
Merge branch 'feat/continuous-profiling-03' of github.com:getsentry/s…
lbloder Sep 5, 2025
b1701c5
Format code
getsentry-bot Sep 5, 2025
30cb14b
add logging to loadProfileConverter
lbloder Sep 8, 2025
94a37b1
Format code
getsentry-bot Sep 8, 2025
524a32a
fix duplication of events
lbloder Sep 8, 2025
2cfe307
catch all exception happening when converting from jfr
lbloder Sep 17, 2025
a559bb3
add exists and writable info to log message
lbloder Sep 19, 2025
94a0097
add method to safely delete file
lbloder Sep 19, 2025
0179ea3
remove setNative call
lbloder Sep 19, 2025
66a6c33
fix test
lbloder Sep 22, 2025
bb10259
fix reference to commit we vendored from
lbloder Sep 22, 2025
7a9c931
drop event if it cannot be processed to not lose the whole chunk
lbloder Sep 23, 2025
bcb7feb
make format
lbloder Sep 23, 2025
0ea8a2d
fix test
lbloder Sep 23, 2025
2e54dde
Format code
getsentry-bot Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ private void stop(final boolean restartProfiler) {
endData.measurementsMap,
endData.traceFile,
startProfileChunkTimestamp,
ProfileChunk.Platform.ANDROID));
ProfileChunk.PLATFORM_ANDROID));
}
}

Expand Down
9 changes: 9 additions & 0 deletions sentry-async-profiler/api/sentry-async-profiler.api
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ public final class io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfi
public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile;
}

public final class io/sentry/asyncprofiler/convert/NonAggregatingEventCollector : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector {
public fun <init> ()V
public fun afterChunk ()V
public fun beforeChunk ()V
public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V
public fun finish ()Z
public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V
}

public final class io/sentry/asyncprofiler/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
public fun <init> (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V
public fun close (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader;
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace;
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event;
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector;
import io.sentry.protocol.SentryStackFrame;
import io.sentry.protocol.profiling.SentryProfile;
import io.sentry.protocol.profiling.SentrySample;
import io.sentry.protocol.profiling.SentryThreadMetadata;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Comment on lines 18 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frame deduplication using HashMap with SentryStackFrame as key relies on the newly added equals/hashCode methods. However, if any of the frame fields are mutable and change after being used as a key, this could lead to inconsistent behavior. Consider making the frame objects immutable after creation or using a different deduplication strategy.

Did we get this right? 👍 / 👎 to inform future reviews.

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter {
private static final long NANOS_PER_SECOND = 1_000_000_000L;
private static final double NANOS_PER_SECOND = 1_000_000_000.0;

private final @NotNull SentryProfile sentryProfile = new SentryProfile();
private final @NotNull SentryStackTraceFactory stackTraceFactory;
private final @NotNull Map<SentryStackFrame, Integer> frameDeduplicationMap = new HashMap<>();
private final @NotNull Map<List<Integer>, Integer> stackDeduplicationMap = new HashMap<>();

public JfrAsyncProfilerToSentryProfileConverter(
JfrReader jfr, Arguments args, @NotNull SentryStackTraceFactory stackTraceFactory) {
Expand All @@ -36,12 +41,18 @@ protected void convertChunk() {
collector.forEach(new ProfileEventVisitor(sentryProfile, stackTraceFactory, jfr, args));
}

@Override
protected EventCollector createCollector(Arguments args) {
return new NonAggregatingEventCollector();
}

public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath)
throws IOException {
JfrAsyncProfilerToSentryProfileConverter converter;
try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) {
Arguments args = new Arguments();
args.cpu = false;
args.wall = true;
args.alloc = false;
args.threads = true;
args.lines = true;
Expand All @@ -56,11 +67,12 @@ protected void convertChunk() {
return converter.sentryProfile;
}

private class ProfileEventVisitor extends AggregatedEventVisitor {
private class ProfileEventVisitor implements EventCollector.Visitor {
private final @NotNull SentryProfile sentryProfile;
private final @NotNull SentryStackTraceFactory stackTraceFactory;
private final @NotNull JfrReader jfr;
private final @NotNull Arguments args;
private final double ticksPerNanosecond;

public ProfileEventVisitor(
@NotNull SentryProfile sentryProfile,
Expand All @@ -71,10 +83,11 @@ public ProfileEventVisitor(
this.stackTraceFactory = stackTraceFactory;
this.jfr = jfr;
this.args = args;
ticksPerNanosecond = jfr.ticksPerSec / NANOS_PER_SECOND;
}

@Override
public void visit(Event event, long value) {
public void visit(Event event, long samples, long value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h any exception here, e.g. when processing a single stack frame would cause the whole chunk to be thrown away if I understand correctly. Is that what we want? Can we only drop broken frames instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How detailed do you think we should do that? just drop the whole event? i.e. catch any exception inside the visit method and just keep going?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opted to drop the event if an error occurs

StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId);
long threadId = resolveThreadId(event.tid);

Expand All @@ -83,12 +96,16 @@ public void visit(Event event, long value) {
processThreadMetadata(event, threadId);
}

createSample(event, threadId);

buildStackTraceAndFrames(stackTrace);
processSampleWithStack(event, threadId, stackTrace);
}
}

private long resolveThreadId(int eventThreadId) {
return jfr.threads.get(eventThreadId) != null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l could extract a var here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m what does it mean if it can't be found in javaThreads? Does it mean it's simply not remapped from the original ID? Could we be reporting garbage data by using the original ID directly if remapping data is missing? e.g. eventThreadId = 9, we can't find it in javaThreads but the real thread ID was actually 117

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, potentially, if the orig. threadId somehow matches a different javaThreadId then we might map it to the wrong thread.
We could set it to a default thread id, like -1 if we can't resolve it. Or drop the event. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending negative ids seems to work and Sentry Backend correctly matches them if both the Thread in the profile and in the Transaction have negative ids.

So both options seem to be valid. However, I'm not sure if it helps to have a "Fallback" Thread that basically gets all unmapped events, as that graph would not make any sense.

I think discarding the event makes more sense, WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set threadId to -1 if it cannot be found in the map in follow-up PR: #4746

? jfr.javaThreads.get(eventThreadId)
: eventThreadId;
}

private void processThreadMetadata(Event event, long threadId) {
final String threadName = getPlainThreadName(event.tid);
sentryProfile
Expand All @@ -103,28 +120,73 @@ private void processThreadMetadata(Event event, long threadId) {
});
}

private void buildStackTraceAndFrames(StackTrace stackTrace) {
List<Integer> stack = new ArrayList<>();
int currentFrame = sentryProfile.getFrames().size();
private void processSampleWithStack(Event event, long threadId, StackTrace stackTrace) {
int stackIndex = addStackTrace(stackTrace);

SentrySample sample = new SentrySample();
sample.setTimestamp(calculateTimestamp(event));
sample.setThreadId(String.valueOf(threadId));
sample.setStackId(stackIndex);

sentryProfile.getSamples().add(sample);
}

private double calculateTimestamp(Event event) {
long nanosFromStart = (long) ((event.time - jfr.chunkStartTicks) / ticksPerNanosecond);

long timeNs = jfr.chunkStartNanos + nanosFromStart;

return DateUtils.nanosToSeconds(timeNs);
}

private int addStackTrace(StackTrace stackTrace) {
List<Integer> callStack = createFramesAndCallStack(stackTrace);

Integer existingIndex = stackDeduplicationMap.get(callStack);
if (existingIndex != null) {
return existingIndex;
}

int stackIndex = sentryProfile.getStacks().size();
sentryProfile.getStacks().add(callStack);
stackDeduplicationMap.put(callStack, stackIndex);
return stackIndex;
}

private List<Integer> createFramesAndCallStack(StackTrace stackTrace) {
List<Integer> callStack = new ArrayList<>();

long[] methods = stackTrace.methods;
byte[] types = stackTrace.types;
int[] locations = stackTrace.locations;

for (int i = 0; i < methods.length; i++) {
StackTraceElement element = getStackTraceElement(methods[i], types[i], locations[i]);
if (element.isNativeMethod()) {
if (element.isNativeMethod() || isNativeFrame(types[i])) {
continue;
}

SentryStackFrame frame = createStackFrame(element);
sentryProfile.getFrames().add(frame);
frame.setNative(isNativeFrame(types[i]));
int frameIndex = getOrAddFrame(frame);
callStack.add(frameIndex);
}

stack.add(currentFrame);
currentFrame++;
return callStack;
}

// Get existing frame index or add new frame and return its index
private int getOrAddFrame(SentryStackFrame frame) {
Integer existingIndex = frameDeduplicationMap.get(frame);

if (existingIndex != null) {
return existingIndex;
}

sentryProfile.getStacks().add(stack);
int newIndex = sentryProfile.getFrames().size();
sentryProfile.getFrames().add(frame);
frameDeduplicationMap.put(frame, newIndex);
return newIndex;
}

private SentryStackFrame createStackFrame(StackTraceElement element) {
Expand Down Expand Up @@ -176,36 +238,12 @@ private boolean isRegularClassWithoutPackage(String className) {
return !className.startsWith("[");
}

private void createSample(Event event, long threadId) {
int stackId = sentryProfile.getStacks().size();
SentrySample sample = new SentrySample();

// Calculate timestamp from JFR event time
long nsFromStart =
(event.time - jfr.chunkStartTicks)
* JfrAsyncProfilerToSentryProfileConverter.NANOS_PER_SECOND
/ jfr.ticksPerSec;
long timeNs = jfr.chunkStartNanos + nsFromStart;
sample.setTimestamp(DateUtils.nanosToSeconds(timeNs));

sample.setThreadId(String.valueOf(threadId));
sample.setStackId(stackId);

sentryProfile.getSamples().add(sample);
}

private boolean shouldMarkAsSystemFrame(StackTraceElement element, String className) {
return element.isNativeMethod() || className.isEmpty();
}

private @Nullable Integer extractLineNumber(StackTraceElement element) {
return element.getLineNumber() != 0 ? element.getLineNumber() : null;
}

private long resolveThreadId(int eventThreadId) {
return jfr.threads.get(eventThreadId) != null
? jfr.javaThreads.get(eventThreadId)
: eventThreadId;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.sentry.asyncprofiler.convert;

import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event;
import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector;
import java.util.ArrayList;
import java.util.List;

public final class NonAggregatingEventCollector implements EventCollector {
final List<Event> events = new ArrayList<>();

@Override
public void collect(Event e) {
events.add(e);
}

@Override
public void beforeChunk() {
// No-op
}

@Override
public void afterChunk() {
// No-op
}

@Override
public boolean finish() {
return false;
}

@Override
public void forEach(Visitor visitor) {
for (Event event : events) {
visitor.visit(event, event.samples(), event.value());
}
}
}
Loading
Loading