diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 1515e18b1ab..82ebe2e6a7a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -301,7 +301,7 @@ private void stop(final boolean restartProfiler) { endData.measurementsMap, endData.traceFile, startProfileChunkTimestamp, - "android")); + ProfileChunk.Platform.ANDROID)); } } diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index 3e11247dd7e..0bf859efb1e 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -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 (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;)V + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;Lio/sentry/SentryStackTraceFactory;)V public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java index f22eb76f709..2fc7bda4775 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -1,5 +1,6 @@ package io.sentry.asyncprofiler.convert; +import io.sentry.DateUtils; import io.sentry.Sentry; import io.sentry.SentryStackTraceFactory; import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments; @@ -8,145 +9,31 @@ import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; import io.sentry.protocol.SentryStackFrame; -import io.sentry.protocol.profiling.JfrSample; import io.sentry.protocol.profiling.SentryProfile; -import io.sentry.protocol.profiling.ThreadMetadata; +import io.sentry.protocol.profiling.SentrySample; +import io.sentry.protocol.profiling.SentryThreadMetadata; import java.io.IOException; import java.nio.file.Path; -import java.time.Instant; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; 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 final @NotNull SentryProfile sentryProfile = new SentryProfile(); + private final @NotNull SentryStackTraceFactory stackTraceFactory; - public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { + public JfrAsyncProfilerToSentryProfileConverter( + JfrReader jfr, Arguments args, @NotNull SentryStackTraceFactory stackTraceFactory) { super(jfr, args); + this.stackTraceFactory = stackTraceFactory; } @Override protected void convertChunk() { - final List events = new ArrayList(); - final List> stacks = new ArrayList<>(); - - collector.forEach( - new AggregatedEventVisitor() { - - @Override - public void visit(Event event, long value) { - events.add(event); - System.out.println(event); - StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); - - if (stackTrace != null) { - Arguments args = JfrAsyncProfilerToSentryProfileConverter.this.args; - long[] methods = stackTrace.methods; - byte[] types = stackTrace.types; - int[] locations = stackTrace.locations; - - if (args.threads) { - if (sentryProfile.threadMetadata == null) { - sentryProfile.threadMetadata = new HashMap<>(); - } - - long threadIdToUse = - jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; - - if (sentryProfile.threadMetadata != null) { - final String threadName = getPlainThreadName(event.tid); - sentryProfile.threadMetadata.computeIfAbsent( - String.valueOf(threadIdToUse), - k -> { - ThreadMetadata metadata = new ThreadMetadata(); - metadata.name = threadName; - metadata.priority = 0; - return metadata; - }); - } - } - - if (sentryProfile.samples == null) { - sentryProfile.samples = new ArrayList<>(); - } - - if (sentryProfile.frames == null) { - sentryProfile.frames = new ArrayList<>(); - } - - List stack = new ArrayList<>(); - int currentStack = stacks.size(); - int currentFrame = sentryProfile.frames != null ? sentryProfile.frames.size() : 0; - for (int i = 0; i < methods.length; i++) { - // for (int i = methods.length; --i >= 0; ) { - SentryStackFrame frame = new SentryStackFrame(); - StackTraceElement element = - getStackTraceElement(methods[i], types[i], locations[i]); - if (element.isNativeMethod()) { - continue; - } - - final String classNameWithLambdas = element.getClassName().replace("/", "."); - frame.setFunction(element.getMethodName()); - - int firstDollar = classNameWithLambdas.indexOf('$'); - String sanitizedClassName = classNameWithLambdas; - if (firstDollar != -1) { - sanitizedClassName = classNameWithLambdas.substring(0, firstDollar); - } - - int lastDot = sanitizedClassName.lastIndexOf('.'); - if (lastDot > 0) { - frame.setModule(sanitizedClassName); - } else if (!classNameWithLambdas.startsWith("[")) { - frame.setModule(""); - } - - if (element.isNativeMethod() || classNameWithLambdas.isEmpty()) { - frame.setInApp(false); - } else { - frame.setInApp( - new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions()) - .isInApp(sanitizedClassName)); - } - - frame.setLineno((element.getLineNumber() != 0) ? element.getLineNumber() : null); - frame.setFilename(classNameWithLambdas); - - if (sentryProfile.frames != null) { - sentryProfile.frames.add(frame); - } - stack.add(currentFrame); - currentFrame++; - } - - long divisor = jfr.ticksPerSec / 1000_000_000L; - long myTimeStamp = - jfr.chunkStartNanos + ((event.time - jfr.chunkStartTicks) / divisor); - - JfrSample sample = new JfrSample(); - Instant instant = Instant.ofEpochSecond(0, myTimeStamp); - double timestampDouble = - instant.getEpochSecond() + instant.getNano() / 1_000_000_000.0; - - sample.timestamp = timestampDouble; - sample.threadId = - String.valueOf( - jfr.threads.get(event.tid) != null - ? jfr.javaThreads.get(event.tid) - : event.tid); - sample.stackId = currentStack; - if (sentryProfile.samples != null) { - sentryProfile.samples.add(sample); - } - - stacks.add(stack); - } - } - }); - sentryProfile.stacks = stacks; - System.out.println("Samples: " + events.size()); + collector.forEach(new ProfileEventVisitor(sentryProfile, stackTraceFactory, jfr, args)); } public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath) @@ -160,10 +47,165 @@ public void visit(Event event, long value) { args.lines = true; args.dot = true; - converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args); + SentryStackTraceFactory stackTraceFactory = + new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions()); + converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory); converter.convert(); } return converter.sentryProfile; } + + private class ProfileEventVisitor extends AggregatedEventVisitor { + private final @NotNull SentryProfile sentryProfile; + private final @NotNull SentryStackTraceFactory stackTraceFactory; + private final @NotNull JfrReader jfr; + private final @NotNull Arguments args; + + public ProfileEventVisitor( + @NotNull SentryProfile sentryProfile, + @NotNull SentryStackTraceFactory stackTraceFactory, + @NotNull JfrReader jfr, + @NotNull Arguments args) { + this.sentryProfile = sentryProfile; + this.stackTraceFactory = stackTraceFactory; + this.jfr = jfr; + this.args = args; + } + + @Override + public void visit(Event event, long value) { + StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + long threadId = resolveThreadId(event.tid); + + if (stackTrace != null) { + if (args.threads) { + processThreadMetadata(event, threadId); + } + + createSample(event, threadId); + + buildStackTraceAndFrames(stackTrace); + } + } + + private void processThreadMetadata(Event event, long threadId) { + final String threadName = getPlainThreadName(event.tid); + sentryProfile + .getThreadMetadata() + .computeIfAbsent( + String.valueOf(threadId), + k -> { + SentryThreadMetadata metadata = new SentryThreadMetadata(); + metadata.setName(threadName); + metadata.setPriority(0); // Default priority + return metadata; + }); + } + + private void buildStackTraceAndFrames(StackTrace stackTrace) { + List stack = new ArrayList<>(); + int currentFrame = sentryProfile.getFrames().size(); + + 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()) { + continue; + } + + SentryStackFrame frame = createStackFrame(element); + sentryProfile.getFrames().add(frame); + + stack.add(currentFrame); + currentFrame++; + } + + sentryProfile.getStacks().add(stack); + } + + private SentryStackFrame createStackFrame(StackTraceElement element) { + SentryStackFrame frame = new SentryStackFrame(); + final String classNameWithLambdas = element.getClassName().replace("/", "."); + frame.setFunction(element.getMethodName()); + + String sanitizedClassName = extractSanitizedClassName(classNameWithLambdas); + frame.setModule(extractModuleName(sanitizedClassName, classNameWithLambdas)); + + if (shouldMarkAsSystemFrame(element, classNameWithLambdas)) { + frame.setInApp(false); + } else { + frame.setInApp(stackTraceFactory.isInApp(sanitizedClassName)); + } + + frame.setLineno(extractLineNumber(element)); + frame.setFilename(classNameWithLambdas); + + return frame; + } + + // Remove lambda suffix from class name + private String extractSanitizedClassName(String classNameWithLambdas) { + int firstDollar = classNameWithLambdas.indexOf('$'); + if (firstDollar != -1) { + return classNameWithLambdas.substring(0, firstDollar); + } + return classNameWithLambdas; + } + + // TODO: test difference between null and empty string for module + private @Nullable String extractModuleName( + String sanitizedClassName, String classNameWithLambdas) { + if (hasPackageStructure(sanitizedClassName)) { + return sanitizedClassName; + } else if (isRegularClassWithoutPackage(classNameWithLambdas)) { + return ""; + } else { + return null; + } + } + + private boolean hasPackageStructure(String className) { + return className.lastIndexOf('.') > 0; + } + + 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; + } + } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java index dd8db6237bc..e76adfa769f 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java @@ -1,7 +1,6 @@ package io.sentry.asyncprofiler.profiling; import static io.sentry.DataCategory.All; -import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; import static java.util.concurrent.TimeUnit.SECONDS; import io.sentry.DataCategory; @@ -148,7 +147,6 @@ public void startProfiler( private void initScopes() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { - // TODO: should we fork the scopes here? this.scopes = Sentry.getCurrentScopes(); final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); if (rateLimiter != null) { @@ -177,14 +175,6 @@ private void start() { return; } - // TODO: Taken from the android profiler, do we need this on the JVM as well? - // If device is offline, we don't start the profiler, to avoid flooding the cache - if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) { - logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler."); - // Let's stop and reset profiler id, as the profile is now broken anyway - stop(false); - return; - } startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now(); } else { startProfileChunkTimestamp = new SentryNanotimeDate(); @@ -195,8 +185,7 @@ private void start() { final String profilingIntervalMicros = String.format("%dus", (int) SECONDS.toMicros(1) / profilingTracesHz); final String command = - String.format( - "start,jfr,event=wall,interval=%s,file=%s", profilingIntervalMicros, filename); + String.format("start,jfr,wall=%s,file=%s", profilingIntervalMicros, filename); System.out.println(command); startData = profiler.execute(command); } catch (Exception e) { @@ -283,14 +272,21 @@ private void stop(final boolean restartProfiler) { // start profiling), meaning there's no scopes to send the chunks. In that case, we store // the data in a list and send it when the next chunk is finished. try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) { + File jfrFile = new File(filename); + // TODO: should we add deleteOnExit() here to let the JVM clean up the file? + // as in `Sentry.java` `initJvmContinuousProfiling` each time we start the profiler we + // create a new + // temp directory/file that we can't cleanup on restart. Unless the user sets + // `profiling-traces-dir-path` manually + // jfrFile.deleteOnExit(); payloadBuilders.add( new ProfileChunk.Builder( profilerId, chunkId, new HashMap<>(), - new File(filename), + jfrFile, startProfileChunkTimestamp, - "java")); + ProfileChunk.Platform.JAVA)); } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java index e721260545b..49d83cffb3d 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java @@ -23,9 +23,6 @@ public final class AsyncProfilerContinuousProfilerProvider int profilingTracesHz, ISentryExecutorService executorService) { return new JavaContinuousProfiler( - logger, - profilingTracesDirPath, - 10, // default profilingTracesHz - executorService); + logger, profilingTracesDirPath, profilingTracesHz, executorService); } } diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt new file mode 100644 index 00000000000..4442553f66c --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt @@ -0,0 +1,85 @@ +package io.sentry.asyncprofiler.convert + +import io.sentry.ILogger +import io.sentry.IProfileConverter +import io.sentry.IScope +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.TracesSampler +import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider +import io.sentry.test.DeferredExecutorService +import java.nio.file.Files +import kotlin.io.path.absolutePathString +import kotlin.io.path.deleteIfExists +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import one.profiler.AsyncProfiler +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever + +class JfrAsyncProfilerToSentryProfileConverterTest { + + private val fixture = Fixture() + + private class Fixture { + private val mockDsn = "http://key@localhost/proj" + val executor = DeferredExecutorService() + val mockedSentry = Mockito.mockStatic(Sentry::class.java) + val mockLogger = mock() + val mockTracesSampler = mock() + + val scopes: IScopes = mock() + val scope: IScope = mock() + + val options = + spy(SentryOptions()).apply { + dsn = mockDsn + profilesSampleRate = 1.0 + isDebug = true + setLogger(mockLogger) + } + + init { + whenever(mockTracesSampler.sampleSessionProfile(any())).thenReturn(true) + } + + fun getSut(optionConfig: ((options: SentryOptions) -> Unit) = {}): IProfileConverter? { + options.executorService = executor + optionConfig(options) + whenever(scopes.options).thenReturn(options) + whenever(scope.options).thenReturn(options) + return AsyncProfilerProfileConverterProvider().profileConverter + } + } + + @BeforeTest + fun `set up`() { + Sentry.setCurrentScopes(fixture.scopes) + + fixture.mockedSentry.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + fixture.mockedSentry.`when` { Sentry.getGlobalScope() }.thenReturn(fixture.scope) + } + + @AfterTest + fun clear() { + Sentry.close() + fixture.mockedSentry.close() + } + + @Test + fun `convert async profiler to sentry`() { + val profiler = AsyncProfiler.getInstance() + val file = Files.createTempFile("sentry-async-profiler-test", ".jfr") + val command = String.format("start,jfr,wall=%s,file=%s", "9900us", file.absolutePathString()) + profiler.execute(command) + profiler.execute("stop,jfr") + + fixture.getSut()!!.convertFromFile(file.toAbsolutePath()) + file.deleteIfExists() + } +} diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt similarity index 90% rename from sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt rename to sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt index 3c895fd7b84..d89a0a2a1aa 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt @@ -1,7 +1,6 @@ -package io.sentry.asyncprofiler +package io.sentry.asyncprofiler.profiling import io.sentry.DataCategory -import io.sentry.IConnectionStatusProvider import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ProfileLifecycle @@ -11,7 +10,6 @@ import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.TracesSampler import io.sentry.TransactionContext -import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.protocol.SentryId import io.sentry.test.DeferredExecutorService import io.sentry.transport.RateLimiter @@ -22,9 +20,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -import kotlin.use import org.mockito.Mockito -import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -41,7 +37,7 @@ class JavaContinuousProfilerTest { private class Fixture { private val mockDsn = "http://key@localhost/proj" val executor = DeferredExecutorService() - val mockedSentry = mockStatic(Sentry::class.java) + val mockedSentry = Mockito.mockStatic(Sentry::class.java) val mockLogger = mock() val mockTracesSampler = mock() @@ -281,25 +277,6 @@ class JavaContinuousProfilerTest { verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to start profiling: "), any()) } - // @Test - // fun `profiler stops profiling and clear scheduled job on close`() { - // val profiler = fixture.getSut() - // profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - // assertTrue(profiler.isRunning) - // - // profiler.close(true) - // assertFalse(profiler.isRunning) - // - // // The timeout scheduled job should be cleared - // val androidProfiler = profiler.getProperty("profiler") - // val scheduledJob = androidProfiler?.getProperty?>("scheduledFinish") - // assertNull(scheduledJob) - // - // val stopFuture = profiler.getStopFuture() - // assertNotNull(stopFuture) - // assertTrue(stopFuture.isCancelled || stopFuture.isDone) - // } - @Test fun `profiler stops and restart for each chunk`() { val profiler = fixture.getSut() @@ -407,24 +384,6 @@ class JavaContinuousProfilerTest { .log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) } - @Test - fun `profiler does not start when offline`() { - val profiler = - fixture.getSut { - it.connectionStatusProvider = mock { provider -> - whenever(provider.connectionStatus) - .thenReturn(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) - } - } - - // If the device is offline, the profiler should never start - profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - assertFalse(profiler.isRunning) - assertEquals(SentryId.EMPTY_ID, profiler.profilerId) - verify(fixture.mockLogger) - .log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) - } - fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java index ae32ced33e6..9b260cfc46c 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -14,6 +14,7 @@ import io.sentry.Baggage; import io.sentry.DateUtils; import io.sentry.IScopes; +import io.sentry.ProfileLifecycle; import io.sentry.PropagationContext; import io.sentry.ScopesAdapter; import io.sentry.Sentry; @@ -82,6 +83,17 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri final @Nullable Boolean sampled = isSampled(otelSpan, samplingDecision); + if (Boolean.TRUE.equals(sampled) && isRootSpan(otelSpan.toSpanData())) { + if (scopes.getOptions().isContinuousProfilingEnabled() + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + scopes + .getOptions() + .getContinuousProfiler() + .startProfiler( + ProfileLifecycle.TRACE, scopes.getOptions().getInternalTracesSampler()); + } + } + final @NotNull PropagationContext propagationContext = new PropagationContext( new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled); @@ -159,6 +171,13 @@ public boolean isStartRequired() { public void onEnd(final @NotNull ReadableSpan spanBeingEnded) { final @Nullable IOtelSpanWrapper sentrySpan = spanStorage.getSentrySpan(spanBeingEnded.getSpanContext()); + + if (isRootSpan(spanBeingEnded.toSpanData()) + && scopes.getOptions().isContinuousProfilingEnabled() + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE); + } + if (sentrySpan != null) { final @NotNull SentryDate finishDate = new SentryLongDate(spanBeingEnded.toSpanData().getEndEpochNanos()); diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts index 307f6e6803f..5afae86bdd3 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.sentryGraphql22) implementation(projects.sentryQuartz) implementation(libs.otel) + implementation(projects.sentryAsyncProfiler) // database query tracing implementation(projects.sentryJdbc) @@ -90,10 +91,17 @@ tasks.register("bootRunWithAgent").configure { val tracesSampleRate = System.getenv("SENTRY_TRACES_SAMPLE_RATE") ?: "1" environment("SENTRY_DSN", dsn) + environment("SENTRY_DEBUG", "true") + environment("SENTRY_PROFILE_SESSION_SAMPLE_RATE", "1.0") + environment("SENTRY_PROFILING_TRACES_DIR_PATH", "tmp/sentry/profiling-traces") + environment("SENTRY_PROFILE_LIFECYCLE", "TRACE") + environment("SENTRY_TRACES_SAMPLE_RATE", tracesSampleRate) environment("OTEL_TRACES_EXPORTER", "none") environment("OTEL_METRICS_EXPORTER", "none") environment("OTEL_LOGS_EXPORTER", "none") + environment("SENTRY_IN_APP_INCLUDES", "io.sentry.samples") + environment("SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT", "false") jvmArgs = listOf("-Dotel.javaagent.debug=true", "-javaagent:$agentJarPath") } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index 009088ce53c..f7643430ecd 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.sentryLogback) implementation(projects.sentryGraphql22) implementation(projects.sentryQuartz) + implementation(projects.sentryAsyncProfiler) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java deleted file mode 100644 index 06c46dd9ab5..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.sentry.samples.spring.boot.jakarta; - -import io.sentry.Sentry; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; - -public class ProfilingInitializer implements ApplicationListener { - private static final Logger LOGGER = LoggerFactory.getLogger(ProfilingInitializer.class); - - // @Override - // public boolean supportsEventType(final @NotNull ResolvableType eventType) { - // return true; - // } - - @Override - public void onApplicationEvent(final @NotNull ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) { - Sentry.startProfiler(); - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 5ece216f281..8050cb8e74c 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -4,7 +4,6 @@ import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; import java.util.Collections; -import org.jetbrains.annotations.NotNull; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; @@ -51,11 +50,6 @@ public JobDetailFactoryBean jobDetail() { return jobDetailFactory; } - @Bean - public @NotNull ProfilingInitializer profilingInitializer() { - return new ProfilingInitializer(); - } - @Bean public SimpleTriggerFactoryBean trigger(JobDetail job) { SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 3fb2f721186..2de573d81aa 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -1,5 +1,5 @@ # NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard -sentry.dsn=https://08c961cc816946f89b4dd69b92e75979@sentry.bloder.dev/3 +sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 sentry.send-default-pii=true sentry.max-request-body-size=medium # Sentry Spring Boot integration allows more fine-grained SentryOptions configuration @@ -18,6 +18,8 @@ sentry.enablePrettySerializationOutput=false in-app-includes="io.sentry.samples" sentry.logs.enabled=true sentry.profile-session-sample-rate=1.0 +sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces +sentry.profile-lifecycle=TRACE # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 7b290a42880..8878607a002 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -11,6 +11,7 @@ import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.NoOpTransportFactory +import io.sentry.ProfileLifecycle import io.sentry.SamplingContext import io.sentry.Sentry import io.sentry.SentryEvent @@ -38,6 +39,7 @@ import io.sentry.transport.ITransport import io.sentry.transport.ITransportGate import io.sentry.transport.apache.ApacheHttpClientTransportFactory import jakarta.servlet.Filter +import java.io.File import java.lang.RuntimeException import kotlin.test.Test import kotlin.test.assertEquals @@ -200,6 +202,9 @@ class SentryAutoConfigurationTest { "sentry.cron.default-failure-issue-threshold=40", "sentry.cron.default-recovery-threshold=50", "sentry.logs.enabled=true", + "sentry.profile-session-sample-rate=1.0", + "sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces", + "sentry.profile-lifecycle=TRACE", ) .run { val options = it.getBean(SentryProperties::class.java) @@ -253,6 +258,10 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) assertThat(options.logs.isEnabled).isEqualTo(true) + assertThat(options.profileSessionSampleRate).isEqualTo(1.0) + assertThat(options.profilingTracesDirPath) + .startsWith(File("tmp/sentry/profiling-traces").absolutePath) + assertThat(options.profileLifecycle).isEqualTo(ProfileLifecycle.TRACE) } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 1c47df15bb0..3aef042bebe 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -11,6 +11,7 @@ import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.NoOpTransportFactory +import io.sentry.ProfileLifecycle import io.sentry.SamplingContext import io.sentry.Sentry import io.sentry.SentryEvent @@ -37,6 +38,7 @@ import io.sentry.spring.tracing.TransactionNameProvider import io.sentry.transport.ITransport import io.sentry.transport.ITransportGate import io.sentry.transport.apache.ApacheHttpClientTransportFactory +import java.io.File import java.lang.RuntimeException import javax.servlet.Filter import kotlin.test.Test @@ -199,6 +201,9 @@ class SentryAutoConfigurationTest { "sentry.cron.default-failure-issue-threshold=40", "sentry.cron.default-recovery-threshold=50", "sentry.logs.enabled=true", + "sentry.profile-session-sample-rate=1.0", + "sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces", + "sentry.profile-lifecycle=TRACE", ) .run { val options = it.getBean(SentryProperties::class.java) @@ -252,6 +257,10 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) assertThat(options.logs.isEnabled).isEqualTo(true) + assertThat(options.profileSessionSampleRate).isEqualTo(1.0) + assertThat(options.profilingTracesDirPath) + .startsWith(File("tmp/sentry/profiling-traces").absolutePath) + assertThat(options.profileLifecycle).isEqualTo(ProfileLifecycle.TRACE) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 78e0dac4f49..0b8f428b1df 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -489,6 +489,7 @@ public final class io/sentry/ExternalOptions { public fun getInAppIncludes ()Ljava/util/List; public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean; + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilingTracesDirPath ()Ljava/lang/String; @@ -532,6 +533,7 @@ public final class io/sentry/ExternalOptions { public fun setIgnoredTransactions (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V public fun setProfilingTracesDirPath (Ljava/lang/String;)V @@ -1934,14 +1936,14 @@ public final class io/sentry/PerformanceCollectionData { public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V - public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/SentryOptions;)V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Ljava/lang/Double;Lio/sentry/ProfileChunk$Platform;Lio/sentry/SentryOptions;)V public fun equals (Ljava/lang/Object;)Z public fun getChunkId ()Lio/sentry/protocol/SentryId; public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; public fun getEnvironment ()Ljava/lang/String; public fun getMeasurements ()Ljava/util/Map; - public fun getPlatform ()Ljava/lang/String; + public fun getPlatform ()Lio/sentry/ProfileChunk$Platform; public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getRelease ()Ljava/lang/String; public fun getSampledProfile ()Ljava/lang/String; @@ -1959,7 +1961,7 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr } public final class io/sentry/ProfileChunk$Builder { - public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;Lio/sentry/SentryDate;Ljava/lang/String;)V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;Lio/sentry/SentryDate;Lio/sentry/ProfileChunk$Platform;)V public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; } @@ -1985,6 +1987,21 @@ public final class io/sentry/ProfileChunk$JsonKeys { public fun ()V } +public final class io/sentry/ProfileChunk$Platform : java/lang/Enum, io/sentry/JsonSerializable { + public static final field ANDROID Lio/sentry/ProfileChunk$Platform; + public static final field JAVA Lio/sentry/ProfileChunk$Platform; + public fun apiName ()Ljava/lang/String; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public static fun valueOf (Ljava/lang/String;)Lio/sentry/ProfileChunk$Platform; + public static fun values ()[Lio/sentry/ProfileChunk$Platform; +} + +public final class io/sentry/ProfileChunk$Platform$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileChunk$Platform; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + public final class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V @@ -6208,91 +6225,79 @@ public final class io/sentry/protocol/ViewHierarchyNode$JsonKeys { public fun ()V } -public final class io/sentry/protocol/profiling/JfrFrame : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public field absPath Ljava/lang/String; - public field filename Ljava/lang/String; - public field function Ljava/lang/String; - public field lineno Ljava/lang/Integer; - public field module Ljava/lang/String; - public fun ()V - public fun getUnknown ()Ljava/util/Map; - public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V - public fun setUnknown (Ljava/util/Map;)V -} - -public final class io/sentry/protocol/profiling/JfrFrame$JsonKeys { - public static final field FILENAME Ljava/lang/String; - public static final field FUNCTION Ljava/lang/String; - public static final field LINE_NO Ljava/lang/String; - public static final field MODULE Ljava/lang/String; - public static final field RAW_FUNCTION Ljava/lang/String; - public fun ()V -} - -public final class io/sentry/protocol/profiling/JfrSample : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public field stackId I - public field threadId Ljava/lang/String; - public field timestamp D +public final class io/sentry/protocol/profiling/SentryProfile : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getFrames ()Ljava/util/List; + public fun getSamples ()Ljava/util/List; + public fun getStacks ()Ljava/util/List; + public fun getThreadMetadata ()Ljava/util/Map; public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setFrames (Ljava/util/List;)V + public fun setSamples (Ljava/util/List;)V + public fun setStacks (Ljava/util/List;)V + public fun setThreadMetadata (Ljava/util/Map;)V public fun setUnknown (Ljava/util/Map;)V } -public final class io/sentry/protocol/profiling/JfrSample$Deserializer : io/sentry/JsonDeserializer { +public final class io/sentry/protocol/profiling/SentryProfile$Deserializer : io/sentry/JsonDeserializer { public fun ()V - public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/JfrSample; + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/SentryProfile; public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } -public final class io/sentry/protocol/profiling/JfrSample$JsonKeys { - public static final field STACK_ID Ljava/lang/String; - public static final field THREAD_ID Ljava/lang/String; - public static final field TIMESTAMP Ljava/lang/String; +public final class io/sentry/protocol/profiling/SentryProfile$JsonKeys { + public static final field FRAMES Ljava/lang/String; + public static final field SAMPLES Ljava/lang/String; + public static final field STACKS Ljava/lang/String; + public static final field THREAD_METADATA Ljava/lang/String; public fun ()V } -public final class io/sentry/protocol/profiling/SentryProfile : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public field frames Ljava/util/List; - public field samples Ljava/util/List; - public field stacks Ljava/util/List; - public field threadMetadata Ljava/util/Map; +public final class io/sentry/protocol/profiling/SentrySample : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getStackId ()I + public fun getThreadId ()Ljava/lang/String; + public fun getTimestamp ()D public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setStackId (I)V + public fun setThreadId (Ljava/lang/String;)V + public fun setTimestamp (D)V public fun setUnknown (Ljava/util/Map;)V } -public final class io/sentry/protocol/profiling/SentryProfile$Deserializer : io/sentry/JsonDeserializer { +public final class io/sentry/protocol/profiling/SentrySample$Deserializer : io/sentry/JsonDeserializer { public fun ()V - public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/SentryProfile; + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/SentrySample; public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } -public final class io/sentry/protocol/profiling/SentryProfile$JsonKeys { - public static final field FRAMES Ljava/lang/String; - public static final field SAMPLES Ljava/lang/String; - public static final field STACKS Ljava/lang/String; - public static final field THREAD_METADATA Ljava/lang/String; +public final class io/sentry/protocol/profiling/SentrySample$JsonKeys { + public static final field STACK_ID Ljava/lang/String; + public static final field THREAD_ID Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public fun ()V } -public final class io/sentry/protocol/profiling/ThreadMetadata : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public field name Ljava/lang/String; - public field priority I +public final class io/sentry/protocol/profiling/SentryThreadMetadata : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getName ()Ljava/lang/String; + public fun getPriority ()I public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setName (Ljava/lang/String;)V + public fun setPriority (I)V public fun setUnknown (Ljava/util/Map;)V } -public final class io/sentry/protocol/profiling/ThreadMetadata$Deserializer : io/sentry/JsonDeserializer { +public final class io/sentry/protocol/profiling/SentryThreadMetadata$Deserializer : io/sentry/JsonDeserializer { public fun ()V - public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/ThreadMetadata; + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/SentryThreadMetadata; public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } -public final class io/sentry/protocol/profiling/ThreadMetadata$JsonKeys { +public final class io/sentry/protocol/profiling/SentryThreadMetadata$JsonKeys { public static final field NAME Ljava/lang/String; public static final field PRIORITY Ljava/lang/String; public fun ()V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index c2ba950c5ef..fbf0be85cf5 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -58,6 +58,7 @@ public final class ExternalOptions { private @Nullable Double profileSessionSampleRate; private @Nullable String profilingTracesDirPath; + private @Nullable ProfileLifecycle profileLifecycle; private @Nullable SentryOptions.Cron cron; @@ -210,6 +211,11 @@ public final class ExternalOptions { options.setProfilingTracesDirPath(propertiesProvider.getProperty("profiling-traces-dir-path")); + String profileLifecycleString = propertiesProvider.getProperty("profile-lifecycle"); + if (profileLifecycleString != null && !profileLifecycleString.isEmpty()) { + options.setProfileLifecycle(ProfileLifecycle.valueOf(profileLifecycleString.toUpperCase())); + } + return options; } @@ -554,4 +560,12 @@ public void setProfileSessionSampleRate(@Nullable Double profileSessionSampleRat public void setProfilingTracesDirPath(@Nullable String profilingTracesDirPath) { this.profilingTracesDirPath = profilingTracesDirPath; } + + public @Nullable ProfileLifecycle getProfileLifecycle() { + return profileLifecycle; + } + + public void setProfileLifecycle(@Nullable ProfileLifecycle profileLifecycle) { + this.profileLifecycle = profileLifecycle; + } } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 40876f4d10b..91628ea25b7 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -11,6 +11,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -25,7 +26,7 @@ public final class ProfileChunk implements JsonUnknown, JsonSerializable { private @NotNull SentryId chunkId; private @Nullable SdkVersion clientSdk; private final @NotNull Map measurements; - private @NotNull String platform; + private @NotNull Platform platform; private @NotNull String release; private @Nullable String environment; private @NotNull String version; @@ -47,7 +48,7 @@ public ProfileChunk() { new File("dummy"), new HashMap<>(), 0.0, - "android", + Platform.ANDROID, SentryOptions.empty()); } @@ -57,7 +58,7 @@ public ProfileChunk( final @NotNull File traceFile, final @NotNull Map measurements, final @NotNull Double timestamp, - final @NotNull String platform, + final @NotNull Platform platform, final @NotNull SentryOptions options) { this.profilerId = profilerId; this.chunkId = chunkId; @@ -96,7 +97,7 @@ public void setDebugMeta(final @Nullable DebugMeta debugMeta) { return environment; } - public @NotNull String getPlatform() { + public @NotNull Platform getPlatform() { return platform; } @@ -179,7 +180,7 @@ public static final class Builder { private final @NotNull File traceFile; private final double timestamp; - private final @NotNull String platform; + private final @NotNull Platform platform; public Builder( final @NotNull SentryId profilerId, @@ -187,7 +188,7 @@ public Builder( final @NotNull Map measurements, final @NotNull File traceFile, final @NotNull SentryDate timestamp, - final @NotNull String platform) { + final @NotNull Platform platform) { this.profilerId = profilerId; this.chunkId = chunkId; this.measurements = new ConcurrentHashMap<>(measurements); @@ -202,6 +203,33 @@ public ProfileChunk build(SentryOptions options) { } } + public enum Platform implements JsonSerializable { + ANDROID, + JAVA; + + public String apiName() { + return name().toLowerCase(Locale.ROOT); + } + + // JsonElementSerializer + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.value(apiName()); + } + + // JsonElementDeserializer + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull Platform deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + return Platform.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); + } + } + } + // JsonSerializable public static final class JsonKeys { @@ -320,7 +348,7 @@ public static final class Deserializer implements JsonDeserializer } break; case JsonKeys.PLATFORM: - String platform = reader.nextStringOrNull(); + Platform platform = reader.nextOrNull(logger, new Platform.Deserializer()); if (platform != null) { data.platform = platform; } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index fa1b7c2a81e..491da449ca1 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -938,6 +938,19 @@ public void flush(long timeoutMillis) { final @NotNull ISpanFactory spanFactory = maybeSpanFactory == null ? getOptions().getSpanFactory() : maybeSpanFactory; + // If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on + // its own. + // Profiler is started before the transaction is created, so that the profiler id is available + // when the transaction starts + if (samplingDecision.getSampled()) { + if (getOptions().isContinuousProfilingEnabled() + && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + getOptions() + .getContinuousProfiler() + .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); + } + } + transaction = spanFactory.createTransaction( transactionContext, this, transactionOptions, compositePerformanceCollector); @@ -960,15 +973,6 @@ public void flush(long timeoutMillis) { transactionProfiler.bindTransaction(transaction); } } - - // If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on - // its own. - if (getOptions().isContinuousProfilingEnabled() - && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { - getOptions() - .getContinuousProfiler() - .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); - } } } if (transactionOptions.isBindToScope()) { diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index ca58bd79487..d72c1cf6921 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -33,11 +33,13 @@ import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; +import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.Properties; @@ -666,25 +668,43 @@ private static void initConfigurations(final @NotNull SentryOptions options) { options.getBackpressureMonitor().start(); } - if (options.isContinuousProfilingEnabled() - && profilingTracesDirPath != null - && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { - final IContinuousProfiler continuousProfiler = - ProfilingServiceLoader.loadContinuousProfiler( - new SystemOutLogger(), - profilingTracesDirPath, - options.getProfilingTracesHz(), - options.getExecutorService()); - - options.setContinuousProfiler(continuousProfiler); - } + initJvmContinuousProfiling(options); options .getLogger() .log( SentryLevel.INFO, - "Continuous profiler is enabled %s", - options.isContinuousProfilingEnabled()); + "Continuous profiler is enabled %s mode: %s", + options.isContinuousProfilingEnabled(), + options.getProfileLifecycle()); + } + + private static void initJvmContinuousProfiling(@NotNull SentryOptions options) { + + if (options.isContinuousProfilingEnabled() + && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { + try { + String profilingTracesDirPath = options.getProfilingTracesDirPath(); + if (profilingTracesDirPath == null) { + profilingTracesDirPath = + Files.createTempDirectory("profiling_traces").toAbsolutePath().toString(); + options.setProfilingTracesDirPath(profilingTracesDirPath); + } + + final IContinuousProfiler continuousProfiler = + ProfilingServiceLoader.loadContinuousProfiler( + new SystemOutLogger(), + profilingTracesDirPath, + options.getProfilingTracesHz(), + options.getExecutorService()); + + options.setContinuousProfiler(continuousProfiler); + } catch (IOException e) { + options + .getLogger() + .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); + } + } } /** Close the SDK */ diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 5d236bfe2c7..0737e2417f7 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -295,7 +295,7 @@ private static void ensureAttachmentSizeLimit( traceFile.getName())); } - if (profileChunk.getPlatform().equals("java")) { + if (ProfileChunk.Platform.JAVA == profileChunk.getPlatform()) { final IProfileConverter profileConverter = ProfilingServiceLoader.loadProfileConverter(); if (profileConverter != null) { @@ -340,7 +340,7 @@ private static void ensureAttachmentSizeLimit( "application-json", traceFile.getName(), null, - profileChunk.getPlatform(), + profileChunk.getPlatform().apiName(), null); // avoid method refs on Android due to some issues with older AGP setups diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index a1a7bb3da4f..830520cf47e 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3240,6 +3240,10 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getProfilingTracesDirPath() != null) { setProfilingTracesDirPath(options.getProfilingTracesDirPath()); } + + if (options.getProfileLifecycle() != null) { + setProfileLifecycle(options.getProfileLifecycle()); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java index ffc779a02dd..087d44f61c0 100644 --- a/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java +++ b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java @@ -16,7 +16,7 @@ public interface JavaContinuousProfilerProvider { /** * Creates and returns a continuous profiler instance. * - * @return a continuous profiler instance, or null if the provider cannot create one + * @return a continuous profiler instance */ @NotNull IContinuousProfiler getContinuousProfiler( diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java deleted file mode 100644 index e013ec594e6..00000000000 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.sentry.protocol.profiling; - -import io.sentry.ILogger; -import io.sentry.JsonSerializable; -import io.sentry.JsonUnknown; -import io.sentry.ObjectWriter; -import java.io.IOException; -import java.util.Map; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class JfrFrame implements JsonUnknown, JsonSerializable { - // @JsonProperty("function") - public @Nullable String function; // e.g., "com.example.MyClass.myMethod" - - // @JsonProperty("module") - public @Nullable String module; // e.g., "com.example" (package name) - - // @JsonProperty("filename") - public @Nullable String filename; // e.g., "MyClass.java" - - // @JsonProperty("lineno") - public @Nullable Integer lineno; // Line number (nullable) - - // @JsonProperty("abs_path") - public @Nullable String absPath; // Optional: Absolute path if available - - public static final class JsonKeys { - public static final String FUNCTION = "function"; - public static final String MODULE = "module"; - public static final String FILENAME = "filename"; - public static final String LINE_NO = "lineno"; - public static final String RAW_FUNCTION = "raw_function"; - } - - @Override - public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { - writer.beginObject(); - - if (function != null) { - writer.name(JsonKeys.FUNCTION).value(logger, function); - } - - if (module != null) { - writer.name(JsonKeys.MODULE).value(logger, module); - } - - if (filename != null) { - writer.name(JsonKeys.FILENAME).value(logger, filename); - } - if (lineno != null) { - writer.name(JsonKeys.LINE_NO).value(logger, lineno); - } - - writer.endObject(); - } - - @Override - public @Nullable Map getUnknown() { - return Map.of(); - } - - @Override - public void setUnknown(@Nullable Map unknown) {} - - // We need equals and hashCode for deduplication if we use Frame objects directly as map keys - // However, it's safer to deduplicate based on the source ResolvedFrame or its components. - // Let's assume we handle deduplication before creating these final Frame objects. -} diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java deleted file mode 100644 index 7c049ce086f..00000000000 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java +++ /dev/null @@ -1,356 +0,0 @@ -// package io.sentry.protocol.profiling; -// -// import io.sentry.EnvelopeReader; -// import io.sentry.JsonSerializer; -// import io.sentry.SentryNanotimeDate; -// import io.sentry.SentryOptions; -// import jdk.jfr.consumer.RecordedClass; -// import jdk.jfr.consumer.RecordedEvent; -// import jdk.jfr.consumer.RecordedFrame; -// import jdk.jfr.consumer.RecordedMethod; -// import jdk.jfr.consumer.RecordedStackTrace; -// import jdk.jfr.consumer.RecordedThread; -// import jdk.jfr.consumer.RecordingFile; -// -// import java.io.File; -// import java.io.IOException; -// import java.io.StringWriter; -// import java.nio.file.Path; -// import java.time.Instant; -// import java.util.ArrayList; -// import java.util.Collections; -// import java.util.HashMap; -// import java.util.List; -// import java.util.Map; -// import java.util.Objects; -// import jdk.jfr.consumer.*; -// -// import java.io.IOException; -// import java.nio.file.Files; // For main method example write -// import java.nio.file.Path; -// import java.time.Instant; -// import java.util.ArrayList; -// import java.util.Collections; -// import java.util.HashMap; -// import java.util.List; -// import java.util.Map; -// import java.util.Objects; -// import java.util.concurrent.ConcurrentHashMap; -// -// public final class JfrToSentryProfileConverter { -// -// // FrameSignature now converts to JfrFrame -// private static class FrameSignature { -// String className; -// String methodName; -// String descriptor; -// String sourceFile; -// int lineNumber; -// -// FrameSignature(RecordedFrame rf) { -// RecordedMethod rm = rf.getMethod(); -// if (rm != null) { -// RecordedClass type = rm.getType(); -// this.className = type != null ? type.getName() : "[unknown_class]"; -// this.methodName = rm.getName(); -// this.descriptor = rm.getDescriptor(); -// } else { -// this.className = "[unknown_class]"; -// this.methodName = "[unknown_method]"; -// this.descriptor = "()V"; -// } -// -// String fileNameFromClass = null; -// if (rf.isJavaFrame() && rm != null && rm.getType() != null) { -// try { fileNameFromClass = rm.getType().getString("sourceFileName"); } -// catch (Exception e) { fileNameFromClass = null; } -// } -// -// if (fileNameFromClass != null && !fileNameFromClass.isEmpty()) { -// this.sourceFile = fileNameFromClass; -// } else if (rf.isJavaFrame() && this.className != null && !this.className.startsWith("[")) { -// int lastDot = this.className.lastIndexOf('.'); -// String simpleClassName = lastDot > 0 ? this.className.substring(lastDot + 1) : -// this.className; -// int firstDollar = simpleClassName.indexOf('$'); -// if (firstDollar > 0) simpleClassName = simpleClassName.substring(0, firstDollar); -// this.sourceFile = simpleClassName + ".java"; -// } else { -// this.sourceFile = "[unknown_source]"; -// } -// if (!rf.isJavaFrame()) this.sourceFile = "[native]"; -// -// this.lineNumber = rf.getInt("lineNumber"); -// if (this.lineNumber < 0) this.lineNumber = 0; -// } -// -// @Override -// public boolean equals(Object o) { -// if (this == o) return true; -// if (!(o instanceof FrameSignature)) return false; -// FrameSignature that = (FrameSignature) o; -// return lineNumber == that.lineNumber && -// Objects.equals(className, that.className) && -// Objects.equals(methodName, that.methodName) && -// Objects.equals(descriptor, that.descriptor) && -// Objects.equals(sourceFile, that.sourceFile); -// } -// -// @Override -// public int hashCode() { -// return Objects.hash(className, methodName, descriptor, sourceFile, lineNumber); -// } -// -// // **** Method now returns JfrFrame **** -// JfrFrame toSentryFrame() { -// JfrFrame frame = new JfrFrame(); // Create JfrFrame instance -// frame.function = this.className + "." + this.methodName; -// -// int lastDot = this.className.lastIndexOf('.'); -// if (lastDot > 0) { -// frame.module = this.className.substring(0, lastDot); -// } else if (!this.className.startsWith("[")) { -// frame.module = ""; -// } -// -// frame.filename = this.sourceFile; -// -// if (this.lineNumber > 0) frame.lineno = this.lineNumber; -// else frame.lineno = null; -// -// if ("[native]".equals(this.sourceFile)) { -// frame.function = "[native_code]"; -// frame.module = null; -// frame.filename = null; -// frame.lineno = null; -// } -// return frame; // Return JfrFrame -// } -// } -// // --- End of FrameSignature --- -// -// private final Map threadNamesByOSId = new ConcurrentHashMap<>(); -// -// public JfrProfile convert(Path jfrFilePath) throws IOException { -// -// // **** Use renamed classes for lists **** -// List samples = new ArrayList<>(); -// List> stacks = new ArrayList<>(); -// List frames = new ArrayList<>(); -// Map threadMetadata = new ConcurrentHashMap<>(); -// -// Map, Integer> stackIdMap = new HashMap<>(); -// Map frameIdMap = new HashMap<>(); -// -// long eventCount = 0; -// long sampleCount = 0; -// long threadsFoundDirectly = 0; -// long threadsFoundInMetadata = 0; -// -// // --- Pre-pass for Thread Metadata --- -// System.out.println("Pre-scanning for thread metadata..."); -// try (RecordingFile recordingFile = new RecordingFile(jfrFilePath)) { -// while (recordingFile.hasMoreEvents()) { -// RecordedEvent event = recordingFile.readEvent(); -// String eventName = event.getEventType().getName(); -// if ("jdk.ThreadStart".equals(eventName)) { -// RecordedThread thread = null; -// try { thread = event.getThread("thread"); } catch(Exception e) { -// // Handle exception if needed -// } -// RecordedThread eventThread = null; -// try { eventThread = event.getThread("eventThread"); } catch(Exception e){ -// // Handle exception if needed -// } -// -// if (thread != null) { -// long osId = thread.getOSThreadId(); -// String name = thread.getJavaName() != null ? thread.getJavaName() : -// thread.getOSName(); -// if (osId > 0 && name != null) threadNamesByOSId.put(osId, name); -// } -// if (eventThread != null) { -// long osId = eventThread.getOSThreadId(); -// String name = eventThread.getJavaName() != null ? eventThread.getJavaName() : -// eventThread.getOSName(); -// if (osId > 0 && name != null) threadNamesByOSId.put(osId, name); -// } -// try { -// long osId = event.getLong("osThreadId"); -// String name = event.getString("threadName"); -// if (osId > 0 && name != null) threadNamesByOSId.put(osId, name); -// } catch (Exception e) {/* ignore */} -// -// } else if ("jdk.JavaThreadStatistics".equals(eventName)) { -// try { -// long osId = event.getLong("osThreadId"); -// String name = event.getString("javaThreadName"); -// if (osId > 0 && name != null) threadNamesByOSId.putIfAbsent(osId, name); -// } catch (Exception e) {/* ignore */} -// } -// } -// } -// System.out.println("Found " + threadNamesByOSId.size() + " thread names during pre-scan."); -// -// // --- Main Processing Pass --- -// System.out.println("Processing execution samples..."); -// try (RecordingFile recordingFile = new RecordingFile(jfrFilePath)) { -// while (recordingFile.hasMoreEvents()) { -// RecordedEvent event = recordingFile.readEvent(); -// eventCount++; -// -// if ("jdk.ExecutionSample".equals(event.getEventType().getName())) { -// sampleCount++; -// Instant timestamp = event.getStartTime(); -// RecordedStackTrace stackTrace = event.getStackTrace(); -// -// if (stackTrace == null) { -// System.err.println("Skipping sample due to missing stacktrace at " + timestamp); -// continue; -// } -// -// // --- Get Thread ID --- -// long osThreadId = -1; -// String threadName = null; -// RecordedThread recordedThread = null; -// try { recordedThread = event.getThread(); } catch (Exception e) { -// // Handle exception if needed -// } -// -// if (recordedThread != null) { -// osThreadId = recordedThread.getOSThreadId(); -// threadsFoundDirectly++; -// } else { -// try { -// if (event.hasField("sampledThread")) { -// RecordedThread eventThreadRef = event.getValue("sampledThread"); -// threadName = eventThreadRef.getJavaName() != null ? eventThreadRef.getJavaName() : -// eventThreadRef.getOSName(); -// if (eventThreadRef != null) osThreadId = eventThreadRef.getOSThreadId(); -// } -//// if (osThreadId <= 0 && event.hasField("tid")) osThreadId = event.getLong("tid"); -//// if (osThreadId <= 0 && event.hasField("osThreadId")) osThreadId = -// event.getLong("osThreadId"); -//// if (osThreadId <= 0) { -//// System.err.println("WARN: Could not determine OS Thread ID for sample at " + -// timestamp + ". Skipping."); -//// continue; -//// } -// threadsFoundInMetadata++; -// } catch (Exception e) { -// System.err.println("WARN: Error accessing thread ID field for sample at " + -// timestamp + ". Skipping. Error: " + e.getMessage()); -// continue; -// } -// } -// -// if (osThreadId <= 0) { -// System.err.println("WARN: Invalid OS Thread ID (<= 0) for sample at " + timestamp + ". -// Skipping."); -// continue; -// } -// String threadIdStr = String.valueOf(osThreadId); -//// final long intermediateThreadId = osThreadId; -// final String intermediateThreadName = threadName; -// // --- Thread Metadata --- -// threadMetadata.computeIfAbsent(threadIdStr, tid -> { -// ThreadMetadata meta = new ThreadMetadata(); -// meta.name = -// intermediateThreadName;//threadNamesByOSId.getOrDefault(intermediateThreadId, "Thread " + tid); -// // meta.priority = ...; // Priority logic if needed -// return meta; -// }); -// -// // --- Stack Trace Processing (Frames and Stacks) --- -// List jfrFrames = stackTrace.getFrames(); -// List currentFrameIds = new ArrayList<>(jfrFrames.size()); -// -// for (RecordedFrame jfrFrame : jfrFrames) { -// FrameSignature sig = new FrameSignature(jfrFrame); -// int frameId = frameIdMap.computeIfAbsent(sig, fSig -> { -// // **** Get JfrFrame from signature **** -// JfrFrame newFrame = fSig.toSentryFrame(); -// frames.add(newFrame); // Add to List -// return frames.size() - 1; -// }); -// currentFrameIds.add(frameId); -// } -// -// Collections.reverse(currentFrameIds); -// -// int stackId = stackIdMap.computeIfAbsent(currentFrameIds, frameIds -> { -// stacks.add(new ArrayList<>(frameIds)); -// return stacks.size() - 1; -// }); -// -// // --- Create Sentry Sample --- -// // **** Create instance of JfrSample **** -// JfrSample sample = new JfrSample(); -// sample.timestamp = timestamp.getEpochSecond() + timestamp.getNano() / 1_000_000_000.0; -// sample.stackId = stackId; -// sample.threadId = threadIdStr; -// samples.add(sample); // Add to List -// } -// } -// } -// -// System.out.println("Processed " + eventCount + " JFR events."); -// System.out.println("Created " + sampleCount + " Sentry samples."); -// System.out.println("Threads found via getThread(): " + threadsFoundDirectly); -// System.out.println("Threads found via field fallback: " + threadsFoundInMetadata); -// System.out.println("Discovered " + frames.size() + " unique frames."); -// System.out.println("Discovered " + stacks.size() + " unique stacks."); -// System.out.println("Discovered " + threadMetadata.size() + " unique threads."); -// -// // --- Assemble final structure --- -// // **** Create instance of JfrProfile **** -// JfrProfile profile = new JfrProfile(); -// profile.samples = samples; -// profile.stacks = stacks; -// profile.frames = frames; -// profile.threadMetadata = new HashMap<>(threadMetadata); // Convert map for final object -// -// return profile; -// -// } -// -// // --- Example Usage (main method remains the same) --- -// public static void main(String[] args) { -// if (args.length < 1) { -// System.err.println("Usage: java JfrToSentryProfileConverter "); -// System.exit(1); -// } -// -// Path jfrPath = new File(args[0]).toPath(); -// JfrToSentryProfileConverter converter = new JfrToSentryProfileConverter(); -// -// SentryOptions options = new SentryOptions(); -// JsonSerializer serializer = new JsonSerializer(options); -// options.setSerializer(serializer); -// options.setEnvelopeReader(new EnvelopeReader(serializer)); -// -// try { -// System.out.println("Parsing JFR file: " + jfrPath.toAbsolutePath()); -// JfrProfile jfrProfile = converter.convert(jfrPath); -// StringWriter writer = new StringWriter(); -// serializer.serialize(jfrProfile, writer); -// String sentryJson = writer.toString(); -// System.out.println("\n--- Sentry Profile JSON ---"); -// System.out.println(sentryJson); -// System.out.println("--- End Sentry Profile JSON ---"); -// -// // Optionally write to a file: -// // Files.writeString(Path.of("sentry_profile.json"), sentryJson); -// // System.out.println("Output written to sentry_profile.json"); -// -// } catch (IOException e) { -// System.err.println("Error processing JFR file: " + e.getMessage()); -// e.printStackTrace(); -// System.exit(1); -// } catch (Exception e) { -// System.err.println("An unexpected error occurred: " + e.getMessage()); -// e.printStackTrace(); -// System.exit(1); -// } -// } -// } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java index ec872072702..ad64b880625 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java @@ -7,45 +7,35 @@ import io.sentry.ObjectReader; import io.sentry.ObjectWriter; import io.sentry.protocol.SentryStackFrame; +import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class SentryProfile implements JsonUnknown, JsonSerializable { - public @Nullable List samples; + private @NotNull List samples = new ArrayList<>(); - public @Nullable List> stacks; // List of frame indices + private @NotNull List> stacks = new ArrayList<>(); // List of frame indices - public @Nullable List frames; + private @NotNull List frames = new ArrayList<>(); // List of stack frames - public @Nullable Map threadMetadata; // Key is Thread ID (String) + private @NotNull Map threadMetadata = + new HashMap<>(); // Key is Thread ID (String) private @Nullable Map unknown; @Override public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); - if (samples != null) { - writer.name(JsonKeys.SAMPLES).value(logger, samples); - } - if (stacks != null) { - writer.name(JsonKeys.STACKS).value(logger, stacks); - } - if (frames != null) { - writer.name(JsonKeys.FRAMES).value(logger, frames); - } - - if (threadMetadata != null) { - writer.name(JsonKeys.THREAD_METADATA).value(logger, threadMetadata); - // writer.beginObject(); - // for (String key : threadMetadata.keySet()) { - // ThreadMetadata value = threadMetadata.get(key); - // writer.name(key).value(logger, value); - // } - // writer.endObject(); - } + writer.name(JsonKeys.SAMPLES).value(logger, samples); + writer.name(JsonKeys.STACKS).value(logger, stacks); + writer.name(JsonKeys.FRAMES).value(logger, frames); + writer.name(JsonKeys.THREAD_METADATA).value(logger, threadMetadata); if (unknown != null) { for (String key : unknown.keySet()) { @@ -56,6 +46,38 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.endObject(); } + public @NotNull List getSamples() { + return samples; + } + + public void setSamples(@NotNull List samples) { + this.samples = samples; + } + + public @NotNull List> getStacks() { + return stacks; + } + + public void setStacks(@NotNull List> stacks) { + this.stacks = stacks; + } + + public @NotNull List getFrames() { + return frames; + } + + public void setFrames(@NotNull List frames) { + this.frames = frames; + } + + public @NotNull Map getThreadMetadata() { + return threadMetadata; + } + + public void setThreadMetadata(@NotNull Map threadMetadata) { + this.threadMetadata = threadMetadata; + } + @Override public @Nullable Map getUnknown() { return unknown; @@ -80,45 +102,72 @@ public static final class Deserializer implements JsonDeserializer unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.FRAMES: + List jfrFrame = + reader.nextListOrNull(logger, new SentryStackFrame.Deserializer()); + if (jfrFrame != null) { + data.frames = jfrFrame; + } + break; + case JsonKeys.SAMPLES: + List sentrySamples = + reader.nextListOrNull(logger, new SentrySample.Deserializer()); + if (sentrySamples != null) { + data.samples = sentrySamples; + } + break; + case JsonKeys.THREAD_METADATA: + Map threadMetadata = + reader.nextMapOrNull(logger, new SentryThreadMetadata.Deserializer()); + if (threadMetadata != null) { + data.threadMetadata = threadMetadata; + } + break; + case JsonKeys.STACKS: + List> jfrStacks = + reader.nextOrNull(logger, new NestedIntegerListDeserializer()); + if (jfrStacks != null) { + data.stacks = jfrStacks; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); return data; - // Map unknown = null; - // - // while (reader.peek() == JsonToken.NAME) { - // final String nextName = reader.nextName(); - // switch (nextName) { - // case JsonKeys.FRAMES: - // List jfrFrame = reader.nextListOrNull(logger, new - // JfrFrame().Deserializer()); - // if (jfrFrame != null) { - // data.frames = jfrFrame; - // } - // break; - // case JsonKeys.SAMPLES: - // List jfrSamples = reader.nextListOrNull(logger, new - // JfrSample().Deserializer()); - // if (jfrSamples != null) { - // data.samples = jfrSamples; - // } - // break; - // - //// case JsonKeys.STACKS: - //// List> jfrStacks = reader.nextListOrNull(logger); - //// if (jfrSamples != null) { - //// data.samples = jfrSamples; - //// } - //// break; - // - // default: - // if (unknown == null) { - // unknown = new ConcurrentHashMap<>(); - // } - // reader.nextUnknown(logger, unknown, nextName); - // break; - // } - // } - // data.setUnknown(unknown); - // reader.endObject(); - // return data; + } + } + + // Custom Deserializer to handle nested Integer list + private static final class NestedIntegerListDeserializer + implements JsonDeserializer>> { + @Override + public @NotNull List> deserialize( + @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + List> result = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + List innerList = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + innerList.add(reader.nextInt()); + } + reader.endArray(); + result.add(innerList); + } + reader.endArray(); + return result; } } } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java similarity index 50% rename from sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java rename to sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java index 14a4b96a867..83e46023e08 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java @@ -6,6 +6,7 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; @@ -14,13 +15,37 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class JfrSample implements JsonUnknown, JsonSerializable { +public final class SentrySample implements JsonUnknown, JsonSerializable { - public double timestamp; // Unix timestamp in seconds with microsecond precision + private double timestamp; - public int stackId; + private int stackId; - public @Nullable String threadId; + private @Nullable String threadId; + + public double getTimestamp() { + return timestamp; + } + + public void setTimestamp(double timestamp) { + this.timestamp = timestamp; + } + + public int getStackId() { + return stackId; + } + + public void setStackId(int stackId) { + this.stackId = stackId; + } + + public @Nullable String getThreadId() { + return threadId; + } + + public void setThreadId(@Nullable String threadId) { + this.threadId = threadId; + } public static final class JsonKeys { public static final String TIMESTAMP = "timestamp"; @@ -54,13 +79,37 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr @Override public void setUnknown(@Nullable Map unknown) {} - public static final class Deserializer implements JsonDeserializer { + public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull JfrSample deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + public @NotNull SentrySample deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { reader.beginObject(); - JfrSample data = new JfrSample(); + SentrySample data = new SentrySample(); + Map unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.TIMESTAMP: + data.timestamp = reader.nextDouble(); + break; + case JsonKeys.STACK_ID: + data.stackId = reader.nextInt(); + break; + case JsonKeys.THREAD_ID: + data.threadId = reader.nextStringOrNull(); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); return data; } } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java similarity index 50% rename from sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java rename to sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java index 9c83a686114..a4c540d3b66 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java @@ -6,16 +6,33 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class ThreadMetadata implements JsonUnknown, JsonSerializable { - public @Nullable String name; // e.g., "com.example.MyClass.myMethod" +public final class SentryThreadMetadata implements JsonUnknown, JsonSerializable { + private @Nullable String name; - public int priority; // e.g., "com.example" (package name) + private int priority; + + public @Nullable String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } public static final class JsonKeys { public static final String NAME = "name"; @@ -40,13 +57,34 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr @Override public void setUnknown(@Nullable Map unknown) {} - public static final class Deserializer implements JsonDeserializer { + public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull ThreadMetadata deserialize( + public @NotNull SentryThreadMetadata deserialize( @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { reader.beginObject(); - ThreadMetadata data = new ThreadMetadata(); + SentryThreadMetadata data = new SentryThreadMetadata(); + Map unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.NAME: + data.name = reader.nextStringOrNull(); + break; + case JsonKeys.PRIORITY: + data.priority = reader.nextInt(); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); return data; } } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 9f32d8cb8f8..9ed3913f715 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -390,6 +390,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with profilingLifecycle set to TRACE`() { + withPropertiesFile("profile-lifecycle=TRACE") { options -> + assertTrue(options.profileLifecycle == ProfileLifecycle.TRACE) + } + } + private fun withPropertiesFile( textLines: List = emptyList(), logger: ILogger = mock(), diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 463b6f5000d..d2b998aa94b 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -968,7 +968,7 @@ class JsonSerializerTest { fixture.traceFile, HashMap(), 5.3, - "android", + ProfileChunk.Platform.ANDROID, fixture.options, ) val measurementNow = SentryNanotimeDate().nanoTimestamp() @@ -1127,7 +1127,7 @@ class JsonSerializerTest { assertEquals(SdkVersion("test", "1.2.3"), profileChunk.clientSdk) assertEquals(chunkId, profileChunk.chunkId) assertEquals("environment", profileChunk.environment) - assertEquals("android", profileChunk.platform) + assertEquals(ProfileChunk.Platform.ANDROID, profileChunk.platform) assertEquals(profilerId, profileChunk.profilerId) assertEquals("release", profileChunk.release) assertEquals("sampled profile in base 64", profileChunk.sampledProfile) diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 716bda99aea..157740bcf0d 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2299,6 +2299,34 @@ class ScopesTest { assertEquals("other.span.origin", transaction.spanContext.origin) } + @Test + fun `startTransaction start the continuous profiler before creating SentryTracer in ProfileLifecycle TRACE`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.profileSessionSampleRate = 1.0 + it.profileLifecycle = ProfileLifecycle.TRACE + } + + whenever(profiler.profilerId).thenReturn(SentryId.EMPTY_ID) + + val expectedSentryId = SentryId() + + doAnswer { whenever(profiler.profilerId).thenReturn(expectedSentryId) } + .whenever(profiler) + .startProfiler(eq(ProfileLifecycle.TRACE), any()) + + val transaction = scopes.startTransaction("test", "test") + + val profilerId = transaction.getData("profiler_id") as? String + val profilingContext = transaction.contexts.get("profile") as? ProfileContext + assertNotNull(profilerId) + assertTrue(SentryId(transaction.getData("profiler_id")!! as String) != SentryId.EMPTY_ID) + assertEquals(expectedSentryId, SentryId(profilerId)) + assertEquals(ProfileContext(SentryId(profilerId)), profilingContext) + verify(profiler).startProfiler(eq(ProfileLifecycle.TRACE), any()) + } + // region profileSession @Test diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 0302843fc3c..c3e563f3a52 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -113,7 +113,7 @@ class SentryClientTest { profilingTraceFile, emptyMap(), 1.0, - "android", + ProfileChunk.Platform.ANDROID, sentryOptions, ) } diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index 026177dfc44..70b8b18de09 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -486,11 +486,11 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("chunk platform") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) - assertEquals("chunk platform", chunk.header.platform) + assertEquals("android", chunk.header.platform) } @Test @@ -499,7 +499,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } file.writeBytes(fixture.bytes) @@ -514,7 +514,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } file.writeBytes(fixture.bytes) @@ -531,7 +531,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } assertFailsWith( @@ -547,7 +547,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } file.writeBytes(fixture.bytes) file.setReadable(false) @@ -565,7 +565,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) @@ -580,7 +580,7 @@ class SentryEnvelopeItemTest { val profileChunk = mock { whenever(it.traceFile).thenReturn(file) - whenever(it.platform).thenReturn("android") + whenever(it.platform).thenReturn(ProfileChunk.Platform.ANDROID) } val exception = diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 8890af5a320..9fe8996f5b1 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -409,6 +409,7 @@ class SentryOptionsTest { externalOptions.isEnableLogs = true externalOptions.profileSessionSampleRate = 0.8 externalOptions.profilingTracesDirPath = "/profiling-traces" + externalOptions.profileLifecycle = ProfileLifecycle.TRACE val hash = StringUtils.calculateStringHash(externalOptions.dsn, mock()) val options = SentryOptions() @@ -466,6 +467,7 @@ class SentryOptionsTest { assertTrue(options.logs.isEnabled!!) assertEquals(0.8, options.profileSessionSampleRate) assertEquals("/profiling-traces${File.separator}${hash}", options.profilingTracesDirPath) + assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle) } @Test diff --git a/sentry/src/test/java/io/sentry/protocol/SentryProfileSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryProfileSerializationTest.kt new file mode 100644 index 00000000000..b605a4a53dd --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SentryProfileSerializationTest.kt @@ -0,0 +1,114 @@ +package io.sentry.protocol + +import io.sentry.FileFromResources +import io.sentry.ILogger +import io.sentry.JsonObjectReader +import io.sentry.JsonObjectWriter +import io.sentry.JsonSerializable +import io.sentry.protocol.profiling.SentryProfile +import io.sentry.protocol.profiling.SentrySample +import io.sentry.protocol.profiling.SentryThreadMetadata +import java.io.StringReader +import java.io.StringWriter +import kotlin.test.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock + +class SentryProfileSerializationTest { + class Fixture { + val logger = mock() + + fun getSut() = + SentryProfile().apply { + samples = + listOf( + SentrySample().apply { + timestamp = 1753439655.387274 + threadId = "57" + stackId = 0 + }, + SentrySample().apply { + timestamp = 1753439655.415672 + threadId = "57" + stackId = 1 + }, + ) + stacks = listOf(listOf(0, 1, 2), listOf(3, 4)) + frames = + listOf( + SentryStackFrame().apply { + filename = "sun.nio.ch.Net" + function = "accept" + module = "sun.nio.ch.Net" + }, + SentryStackFrame().apply { + filename = "org.apache.tomcat.util.net.NioEndpoint" + function = "serverSocketAccept" + module = "org.apache.tomcat.util.net.NioEndpoint" + lineno = 519 + }, + SentryStackFrame().apply { + filename = "java.lang.Thread" + function = "run" + module = "java.lang.Thread" + lineno = 840 + }, + SentryStackFrame().apply { + filename = "io.sentry.samples.spring.boot.jakarta.quartz.SampleJob" + function = "execute" + module = "io.sentry.samples.spring.boot.jakarta.quartz.SampleJob" + lineno = 14 + isInApp = true + }, + SentryStackFrame().apply { + filename = "" + function = "Unsafe_Park" + module = "" + isInApp = false + }, + ) + threadMetadata = + mapOf( + "57" to + SentryThreadMetadata().apply { + name = "http-nio-8080-Acceptor" + priority = 0 + } + ) + } + } + + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = sanitizedFile("json/sentry_profile.json") + val actual = serialize(fixture.getSut()) + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = sanitizedFile("json/sentry_profile.json") + val actual = deserialize(expectedJson) + val actualJson = serialize(actual) + assertEquals(expectedJson, actualJson) + } + + // Helper + + private fun sanitizedFile(path: String): String = + FileFromResources.invoke(path).replace(Regex("[\n\r]"), "").replace(" ", "") + + private fun serialize(jsonSerializable: JsonSerializable): String { + val wrt = StringWriter() + val jsonWrt = JsonObjectWriter(wrt, 100) + jsonSerializable.serialize(jsonWrt, fixture.logger) + return wrt.toString() + } + + private fun deserialize(json: String): SentryProfile { + val reader = JsonObjectReader(StringReader(json)) + return SentryProfile.Deserializer().deserialize(reader, fixture.logger) + } +} diff --git a/sentry/src/test/resources/json/sentry_profile.json b/sentry/src/test/resources/json/sentry_profile.json new file mode 100644 index 00000000000..503f4c0e4ba --- /dev/null +++ b/sentry/src/test/resources/json/sentry_profile.json @@ -0,0 +1,63 @@ +{ + "samples": [ + { + "timestamp": 1753439655.387274, + "stack_id": 0, + "thread_id": "57" + }, + { + "timestamp": 1753439655.415672, + "stack_id": 1, + "thread_id": "57" + } + ], + "stacks": [ + [ + 0, + 1, + 2 + ], + [ + 3, + 4 + ] + ], + "frames": [ + { + "filename": "sun.nio.ch.Net", + "function": "accept", + "module": "sun.nio.ch.Net" + }, + { + "filename": "org.apache.tomcat.util.net.NioEndpoint", + "function": "serverSocketAccept", + "module": "org.apache.tomcat.util.net.NioEndpoint", + "lineno": 519 + }, + { + "filename": "java.lang.Thread", + "function": "run", + "module": "java.lang.Thread", + "lineno": 840 + }, + { + "filename": "io.sentry.samples.spring.boot.jakarta.quartz.SampleJob", + "function": "execute", + "module": "io.sentry.samples.spring.boot.jakarta.quartz.SampleJob", + "lineno": 14, + "in_app": true + }, + { + "filename": "", + "function": "Unsafe_Park", + "module": "", + "in_app": false + } + ], + "thread_metadata": { + "57": { + "name": "http-nio-8080-Acceptor", + "priority": 0 + } + } +}