Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion sentry-async-profiler/api/sentry-async-profiler.api
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public final class io/sentry/asyncprofiler/BuildConfig {
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.sentry.asyncprofiler.convert;

import io.sentry.DateUtils;
import io.sentry.ILogger;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryStackTraceFactory;
import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments;
import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.JfrConverter;
Expand All @@ -24,16 +26,22 @@

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

private final @NotNull SentryProfile sentryProfile = new SentryProfile();
private final @NotNull SentryStackTraceFactory stackTraceFactory;
private final @NotNull ILogger logger;
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) {
JfrReader jfr,
Arguments args,
@NotNull SentryStackTraceFactory stackTraceFactory,
@NotNull ILogger logger) {
super(jfr, args);
this.stackTraceFactory = stackTraceFactory;
this.logger = logger;
}

@Override
Expand All @@ -60,7 +68,9 @@ protected EventCollector createCollector(Arguments args) {

SentryStackTraceFactory stackTraceFactory =
new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions());
converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory);
ILogger logger = Sentry.getGlobalScope().getOptions().getLogger();
converter =
new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory, logger);
converter.convert();
}

Expand Down Expand Up @@ -88,25 +98,32 @@ public ProfileEventVisitor(

@Override
public void visit(Event event, long samples, long value) {
StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId);
long threadId = resolveThreadId(event.tid);
try {
StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId);
long threadId = resolveThreadId(event.tid);

if (stackTrace != null) {
if (args.threads) {
processThreadMetadata(event, threadId);
}
if (stackTrace != null) {
if (args.threads) {
processThreadMetadata(event, threadId);
}

processSampleWithStack(event, threadId, stackTrace);
processSampleWithStack(event, threadId, stackTrace);
}
} catch (Exception e) {
logger.log(SentryLevel.WARNING, "Failed to process JFR event " + event, e);
}
}

private long resolveThreadId(int eventThreadId) {
return jfr.threads.get(eventThreadId) != null
? jfr.javaThreads.get(eventThreadId)
: eventThreadId;
private long resolveThreadId(int eventId) {
Long javaThreadId = jfr.javaThreads.get(eventId);
return javaThreadId != null ? javaThreadId : UNKNOWN_THREAD_ID;
Copy link

Choose a reason for hiding this comment

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

Bug: Thread ID Resolution Issue

The resolveThreadId method now returns UNKNOWN_THREAD_ID (-1) when jfr.javaThreads lacks an entry for an event's thread. Previously, it would fall back to the original eventThreadId. This change means valid thread IDs without javaThreads metadata are lost, causing their associated samples and thread metadata to be skipped.

Fix in Cursor Fix in Web

}

private void processThreadMetadata(Event event, long threadId) {
if (threadId == UNKNOWN_THREAD_ID) {
return;
}

final String threadName = getPlainThreadName(event.tid);
sentryProfile
.getThreadMetadata()
Expand Down Expand Up @@ -167,7 +184,6 @@ private List<Integer> createFramesAndCallStack(StackTrace stackTrace) {
}

SentryStackFrame frame = createStackFrame(element);
frame.setNative(isNativeFrame(types[i]));
int frameIndex = getOrAddFrame(frame);
callStack.add(frameIndex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ public final class JavaContinuousProfiler

private @NotNull String filename = "";

private @Nullable AsyncProfiler profiler;
private @NotNull AsyncProfiler profiler;
private volatile boolean shouldSample = true;
private boolean shouldStop = false;
private boolean isSampled = false;
private int rootSpanCounter = 0;

Expand All @@ -69,27 +68,20 @@ public JavaContinuousProfiler(
final @NotNull ILogger logger,
final @Nullable String profilingTracesDirPath,
final int profilingTracesHz,
final @NotNull ISentryExecutorService executorService) {
final @NotNull ISentryExecutorService executorService)
throws Exception {
this.logger = logger;
this.profilingTracesDirPath = profilingTracesDirPath;
this.profilingTracesHz = profilingTracesHz;
this.executorService = executorService;
initializeProfiler();
}

private void initializeProfiler() {
try {
this.profiler = AsyncProfiler.getInstance();
// Check version to verify profiler is working
String version = profiler.execute("version");
logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version);
} catch (Exception e) {
logger.log(
SentryLevel.WARNING,
"Failed to initialize AsyncProfiler. Profiling will be disabled.",
e);
this.profiler = null;
}
private void initializeProfiler() throws Exception {
this.profiler = AsyncProfiler.getInstance();
// Check version to verify profiler is working
String version = profiler.execute("version");
logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version);
}

private boolean init() {
Expand All @@ -98,11 +90,6 @@ private boolean init() {
}
isInitialized = true;

if (profiler == null) {
logger.log(SentryLevel.ERROR, "Disabling profiling because AsyncProfiler is not available.");
return false;
}

if (profilingTracesDirPath == null) {
logger.log(
SentryLevel.WARNING,
Expand All @@ -115,8 +102,10 @@ private boolean init() {
if (!profileDir.canWrite() || !profileDir.exists()) {
logger.log(
SentryLevel.WARNING,
"Disabling profiling because traces directory is not writable or does not exist: %s",
profilingTracesDirPath);
"Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)",
profilingTracesDirPath,
profileDir.canWrite(),
profileDir.exists());
return false;
}

Expand Down Expand Up @@ -166,10 +155,11 @@ public void startProfiler(
}

if (!isRunning()) {
shouldStop = false;
logger.log(SentryLevel.DEBUG, "Started Profiler.");
start();
}
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Error starting profiler: ", e);
}
}

Expand Down Expand Up @@ -208,11 +198,6 @@ private void start() {
startProfileChunkTimestamp = new SentryNanotimeDate();
}

if (profiler == null) {
logger.log(SentryLevel.ERROR, "Cannot start profiling: AsyncProfiler is not available");
return;
}

filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr";

File jfrFile = new File(filename);
Expand All @@ -231,9 +216,7 @@ private void start() {
logger.log(SentryLevel.ERROR, "Failed to start profiling: ", e);
filename = "";
// Try to clean up the file if it was created
if (jfrFile.exists()) {
jfrFile.delete();
}
safelyRemoveFile(jfrFile);
return;
}

Expand All @@ -250,7 +233,8 @@ private void start() {
SentryLevel.ERROR,
"Failed to schedule profiling chunk finish. Did you call Sentry.close()?",
e);
shouldStop = true;
// If we can't schedule the auto-stop, stop immediately without restart
stop(false);
}
}

Expand All @@ -269,10 +253,12 @@ public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) {
if (rootSpanCounter < 0) {
rootSpanCounter = 0;
}
shouldStop = true;
// Stop immediately without restart
stop(false);
break;
case MANUAL:
shouldStop = true;
// Stop immediately without restart
stop(false);
break;
}
}
Expand All @@ -293,19 +279,12 @@ private void stop(final boolean restartProfiler) {

File jfrFile = new File(filename);

if (profiler == null) {
logger.log(SentryLevel.WARNING, "Profiler is null when trying to stop");
return;
}

try {
profiler.execute("stop,jfr");
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Error stopping profiler, attempting cleanup: ", e);
// Clean up file if it exists
if (jfrFile.exists()) {
jfrFile.delete();
}
safelyRemoveFile(jfrFile);
}

// The scopes can be null if the profiler is started before the SDK is initialized (app
Expand All @@ -330,9 +309,7 @@ private void stop(final boolean restartProfiler) {
jfrFile.exists(),
jfrFile.canRead(),
jfrFile.length());
if (jfrFile.exists()) {
jfrFile.delete();
}
safelyRemoveFile(jfrFile);
}

// Always clean up state, even if stop failed
Expand All @@ -343,14 +320,16 @@ private void stop(final boolean restartProfiler) {
sendChunks(scopes, scopes.getOptions());
}

if (restartProfiler && !shouldStop) {
if (restartProfiler) {
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
start();
} else {
// When the profiler is stopped manually, we have to reset its id
profilerId = SentryId.EMPTY_ID;
logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
}
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Error stopping profiler: ", e);
}
}

Expand All @@ -363,9 +342,8 @@ public void reevaluateSampling() {
public void close(final boolean isTerminating) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
rootSpanCounter = 0;
shouldStop = true;
stop(false);
if (isTerminating) {
stop(false);
isClosed.set(true);
}
}
Expand Down Expand Up @@ -403,10 +381,20 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti
}
}

private void safelyRemoveFile(File file) {
try {
if (file.exists()) {
file.delete();
}
} catch (Exception e) {
logger.log(SentryLevel.INFO, "Failed to remove jfr file %s.", file.getAbsolutePath(), e);
}
}

@Override
public boolean isRunning() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
return isRunning && profiler != null && !filename.isEmpty();
return isRunning && !filename.isEmpty();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.sentry.IContinuousProfiler;
import io.sentry.ILogger;
import io.sentry.ISentryExecutorService;
import io.sentry.NoOpContinuousProfiler;
import io.sentry.SentryLevel;
import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler;
import io.sentry.profiling.JavaContinuousProfilerProvider;
import io.sentry.profiling.JavaProfileConverterProvider;
Expand All @@ -22,7 +24,15 @@ public final class AsyncProfilerContinuousProfilerProvider
String profilingTracesDirPath,
int profilingTracesHz,
ISentryExecutorService executorService) {
return new JavaContinuousProfiler(
logger, profilingTracesDirPath, profilingTracesHz, executorService);
try {
return new JavaContinuousProfiler(
logger, profilingTracesDirPath, profilingTracesHz, executorService);
} catch (Exception e) {
logger.log(
SentryLevel.WARNING,
"Failed to initialize AsyncProfiler. Profiling will be disabled.",
e);
return NoOpContinuousProfiler.getInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Vendored AsyncProfiler code for converting JFR Files
- Vendored-in from commit fe1bc66d4b6181413847f6bbe5c0db805f3e9194 of repository: git@github.com:async-profiler/async-profiler.git
- Vendored-in from commit https://github.com/async-profiler/async-profiler/tree/fe1bc66d4b6181413847f6bbe5c0db805f3e9194
- Only the code related to JFR conversion is included.
- The `AsyncProfiler` itself is included as a dependency in the Maven project.
Loading
Loading