From f32782c7b2f1da871b623f21dd2150adbf013ee2 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 25 Apr 2025 12:14:54 +0200 Subject: [PATCH 01/31] wip --- 197d8e97cb514418b15e5578026f39f2.jfr | Bin 0 -> 84069 bytes 249fcba726d5464b90d2dd4b2b24ad91.jfr | Bin 0 -> 99364 bytes 36354ee63d9240659b46ca78579a5c64.jfr | Bin 0 -> 53647 bytes bbc481b114554993b24a753fc6874fe6.jfr | Bin 0 -> 88140 bytes .../src/main/AndroidManifest.xml | 2 +- .../boot/jakarta/ProfilingInitializer.java | 27 + .../boot/jakarta/SentryDemoApplication.java | 7 + .../src/main/resources/application.properties | 4 +- sentry/build.gradle.kts | 2 + .../src/main/java/io/sentry/ProfileChunk.java | 48 +- .../java/io/sentry/SentryEnvelopeItem.java | 27 +- .../main/java/io/sentry/SentryOptions.java | 4 +- .../protocol/jfr/convert/Arguments.java | 128 ++++ .../protocol/jfr/convert/CallStack.java | 32 + .../protocol/jfr/convert/Classifier.java | 146 ++++ .../protocol/jfr/convert/FlameGraph.java | 395 ++++++++++ .../io/sentry/protocol/jfr/convert/Frame.java | 65 ++ .../io/sentry/protocol/jfr/convert/Index.java | 47 ++ .../protocol/jfr/convert/JfrConverter.java | 275 +++++++ .../protocol/jfr/convert/JfrToFlame.java | 91 +++ .../protocol/jfr/convert/JfrToHeatmap.java | 96 +++ .../jfr/convert/ResourceProcessor.java | 38 + .../io/sentry/protocol/jfr/jfr/ClassRef.java | 14 + .../sentry/protocol/jfr/jfr/Dictionary.java | 116 +++ .../protocol/jfr/jfr/DictionaryInt.java | 125 ++++ .../io/sentry/protocol/jfr/jfr/Element.java | 12 + .../io/sentry/protocol/jfr/jfr/JfrClass.java | 40 + .../io/sentry/protocol/jfr/jfr/JfrField.java | 20 + .../io/sentry/protocol/jfr/jfr/JfrReader.java | 685 ++++++++++++++++++ .../io/sentry/protocol/jfr/jfr/MethodRef.java | 18 + .../sentry/protocol/jfr/jfr/StackTrace.java | 18 + .../jfr/jfr/event/AllocationSample.java | 43 ++ .../protocol/jfr/jfr/event/CPULoad.java | 21 + .../protocol/jfr/jfr/event/ContendedLock.java | 41 ++ .../sentry/protocol/jfr/jfr/event/Event.java | 62 ++ .../jfr/jfr/event/EventAggregator.java | 149 ++++ .../jfr/jfr/event/EventCollector.java | 24 + .../jfr/jfr/event/ExecutionSample.java | 27 + .../protocol/jfr/jfr/event/GCHeapSummary.java | 28 + .../protocol/jfr/jfr/event/LiveObject.java | 43 ++ .../protocol/jfr/jfr/event/MallocEvent.java | 22 + .../jfr/jfr/event/MallocLeakAggregator.java | 65 ++ .../protocol/jfr/jfr/event/ObjectCount.java | 23 + .../profiling/JavaContinuousProfiler.java | 353 +++++++++ .../sentry/protocol/profiling/JfrFrame.java | 70 ++ .../sentry/protocol/profiling/JfrProfile.java | 130 ++++ .../sentry/protocol/profiling/JfrSample.java | 63 ++ .../JfrToSentryProfileConverter.java | 347 +++++++++ .../protocol/profiling/ThreadMetadata.java | 57 ++ .../test/java/io/sentry/JavaProfilerTest.kt | 36 + sentry/test88-20250408-152005.jfr | Bin 0 -> 57839 bytes sentry/test88-20250408-152039.jfr | Bin 0 -> 56883 bytes sentry/test88-20250408-152146.jfr | Bin 0 -> 85992 bytes 53 files changed, 4071 insertions(+), 15 deletions(-) create mode 100644 197d8e97cb514418b15e5578026f39f2.jfr create mode 100644 249fcba726d5464b90d2dd4b2b24ad91.jfr create mode 100644 36354ee63d9240659b46ca78579a5c64.jfr create mode 100644 bbc481b114554993b24a753fc6874fe6.jfr create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java create mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java create mode 100644 sentry/src/test/java/io/sentry/JavaProfilerTest.kt create mode 100644 sentry/test88-20250408-152005.jfr create mode 100644 sentry/test88-20250408-152039.jfr create mode 100644 sentry/test88-20250408-152146.jfr diff --git a/197d8e97cb514418b15e5578026f39f2.jfr b/197d8e97cb514418b15e5578026f39f2.jfr new file mode 100644 index 0000000000000000000000000000000000000000..fd255d938c5e68ab4d070642fe762d83ec2625c6 GIT binary patch literal 84069 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 literal 0 HcmV?d00001 diff --git a/249fcba726d5464b90d2dd4b2b24ad91.jfr b/249fcba726d5464b90d2dd4b2b24ad91.jfr new file mode 100644 index 0000000000000000000000000000000000000000..9b54f947367c2f4c3311361478c1c8e08dcba81c GIT binary patch literal 99364 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 literal 0 HcmV?d00001 diff --git a/36354ee63d9240659b46ca78579a5c64.jfr b/36354ee63d9240659b46ca78579a5c64.jfr new file mode 100644 index 0000000000000000000000000000000000000000..52073814033e0c800a042c1454ac52f2e27617e1 GIT binary patch literal 53647 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 G+WWt8T);H| literal 0 HcmV?d00001 diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index dbda0b0a1a2..30254aff745 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -67,7 +67,7 @@ android:exported="false"/> - + 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 new file mode 100644 index 00000000000..f840f3d76e9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java @@ -0,0 +1,27 @@ +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 8050cb8e74c..03c3b68efd2 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,6 +4,8 @@ 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; @@ -50,6 +52,11 @@ 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 e00d4c855ea..8ff79d1ab85 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://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +sentry.dsn=https://08c961cc816946f89b4dd69b92e75979@sentry.bloder.dev/3 sentry.send-default-pii=true sentry.max-request-body-size=medium # Sentry Spring Boot integration allows more fine-grained SentryOptions configuration @@ -13,7 +13,7 @@ sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR sentry.enable-backpressure-handling=true -sentry.enable-spotlight=true +sentry.enable-spotlight=false sentry.enablePrettySerializationOutput=false in-app-includes="io.sentry.samples" diff --git a/sentry/build.gradle.kts b/sentry/build.gradle.kts index 8b183cf50bb..c0ad5bada1d 100644 --- a/sentry/build.gradle.kts +++ b/sentry/build.gradle.kts @@ -20,6 +20,8 @@ dependencies { errorprone(Config.CompileOnly.errorprone) compileOnly(Config.CompileOnly.jetbrainsAnnotations) errorprone(Config.CompileOnly.errorProneNullAway) + // https://mvnrepository.com/artifact/tools.profiler/async-profiler + implementation("tools.profiler:async-profiler:3.0") // tests testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 89d9293f5c2..77fb5c51371 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -4,6 +4,7 @@ import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; +import io.sentry.protocol.profiling.JfrProfile; import io.sentry.vendor.gson.stream.JsonToken; import java.io.File; import java.io.IOException; @@ -33,6 +34,8 @@ public final class ProfileChunk implements JsonUnknown, JsonSerializable { /** Profile trace encoded with Base64. */ private @Nullable String sampledProfile = null; + private @Nullable JfrProfile jfrProfile; + private @Nullable Map unknown; public ProfileChunk() { @@ -60,7 +63,28 @@ public ProfileChunk( this.clientSdk = options.getSdkVersion(); this.release = options.getRelease() != null ? options.getRelease() : ""; this.environment = options.getEnvironment(); - this.platform = "android"; + this.platform = "java"; + this.version = "2"; + this.timestamp = timestamp; + } + + public ProfileChunk( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull Double timestamp, + final @NotNull String platform, + final @NotNull SentryOptions options) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.traceFile = traceFile; + this.measurements = measurements; + this.debugMeta = null; + this.clientSdk = options.getSdkVersion(); + this.release = options.getRelease() != null ? options.getRelease() : ""; + this.environment = options.getEnvironment(); + this.platform = platform; this.version = "2"; this.timestamp = timestamp; } @@ -121,6 +145,14 @@ public double getTimestamp() { return version; } + public @Nullable JfrProfile getJfrProfile() { + return jfrProfile; + } + + public void setJfrProfile(@Nullable JfrProfile jfrProfile) { + this.jfrProfile = jfrProfile; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -136,7 +168,8 @@ public boolean equals(Object o) { && Objects.equals(environment, that.environment) && Objects.equals(version, that.version) && Objects.equals(sampledProfile, that.sampledProfile) - && Objects.equals(unknown, that.unknown); + && Objects.equals(unknown, that.unknown) + && Objects.equals(jfrProfile, that.jfrProfile); } @Override @@ -152,6 +185,7 @@ public int hashCode() { environment, version, sampledProfile, + jfrProfile, unknown); } @@ -194,6 +228,7 @@ public static final class JsonKeys { public static final String VERSION = "version"; public static final String SAMPLED_PROFILE = "sampled_profile"; public static final String TIMESTAMP = "timestamp"; + public static final String JRF_PROFILE = "profile"; } @Override @@ -221,6 +256,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); } writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); + if (jfrProfile != null) { + writer.name(JsonKeys.JRF_PROFILE).value(logger, jfrProfile); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -320,6 +358,12 @@ public static final class Deserializer implements JsonDeserializer data.timestamp = timestamp; } break; + case JsonKeys.JRF_PROFILE: + JfrProfile jfrProfile = reader.nextOrNull(logger, new JfrProfile.Deserializer()); + if (jfrProfile != null) { + data.jfrProfile = jfrProfile; + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 43ededf6a88..b6821f71a5d 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -7,6 +7,8 @@ import io.sentry.clientreport.ClientReport; import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SentryTransaction; +import io.sentry.protocol.profiling.JfrProfile; +import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; @@ -16,6 +18,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -282,24 +285,30 @@ private static void ensureAttachmentSizeLimit( "Dropping profile chunk, because the file '%s' doesn't exists", traceFile.getName())); } - // The payload of the profile item is a json including the trace file encoded with - // base64 - final byte[] traceFileBytes = + if(traceFile.getName().endsWith(".jfr")) { + JfrProfile profile = new JfrToSentryProfileConverter().convert(traceFile.toPath()); + profileChunk.setJfrProfile(profile); + + } else { + // The payload of the profile item is a json including the trace file encoded with + // base64 + final byte[] traceFileBytes = readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); - final @NotNull String base64Trace = + final @NotNull String base64Trace = Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); - if (base64Trace.isEmpty()) { - throw new SentryEnvelopeException("Profiling trace file is empty"); + if (base64Trace.isEmpty()) { + throw new SentryEnvelopeException("Profiling trace file is empty"); + } + profileChunk.setSampledProfile(base64Trace); } - profileChunk.setSampledProfile(base64Trace); try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { serializer.serialize(profileChunk, writer); return stream.toByteArray(); } catch (IOException e) { throw new SentryEnvelopeException( - String.format("Failed to serialize profile chunk\n%s", e.getMessage())); + String.format("Failed to serialize profile chunk\n%s", e.getMessage())); } finally { // In any case we delete the trace file traceFile.delete(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d5623d44f26..bc01399b2c8 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -16,6 +16,7 @@ import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryTransaction; +import io.sentry.protocol.profiling.JavaContinuousProfiler; import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; @@ -548,7 +549,7 @@ public class SentryOptions { * means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null * (disabled). */ - private @Nullable Double profileSessionSampleRate; + private @Nullable Double profileSessionSampleRate = 1.0; /** * Whether the profiling lifecycle is controlled manually or based on the trace lifecycle. @@ -3002,6 +3003,7 @@ private SentryOptions(final boolean empty) { setSentryClientName(BuildConfig.SENTRY_JAVA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(sdkVersion); addPackageInfo(); + setContinuousProfiler(new JavaContinuousProfiler(new SystemOutLogger(), "", 10, executorService)); } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java new file mode 100644 index 00000000000..7b48136ea24 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java @@ -0,0 +1,128 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.regex.Pattern; + +public class Arguments { + public String title = "Flame Graph"; + public String highlight; + public String output; + public String state; + public Pattern include; + public Pattern exclude; + public double minwidth; + public double grain; + public int skip; + public boolean help; + public boolean reverse; + public boolean inverted; + public boolean cpu; + public boolean wall; + public boolean alloc; + public boolean nativemem; + public boolean leak; + public boolean live; + public boolean lock; + public boolean threads; + public boolean classify; + public boolean total; + public boolean lines; + public boolean bci; + public boolean simple; + public boolean norm; + public boolean dot; + public long from; + public long to; + public final List files = new ArrayList<>(); + + public Arguments(String... args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + String fieldName; + if (arg.startsWith("--")) { + fieldName = arg.substring(2); + } else if (arg.startsWith("-") && arg.length() == 2) { + fieldName = alias(arg.charAt(1)); + } else { + files.add(arg); + continue; + } + + try { + Field f = Arguments.class.getDeclaredField(fieldName); + if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) { + throw new IllegalArgumentException(arg); + } + + Class type = f.getType(); + if (type == String.class) { + f.set(this, args[++i]); + } else if (type == boolean.class) { + f.setBoolean(this, true); + } else if (type == int.class) { + f.setInt(this, Integer.parseInt(args[++i])); + } else if (type == double.class) { + f.setDouble(this, Double.parseDouble(args[++i])); + } else if (type == long.class) { + f.setLong(this, parseTimestamp(args[++i])); + } else if (type == Pattern.class) { + f.set(this, Pattern.compile(args[++i])); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalArgumentException(arg); + } + } + } + + private static String alias(char c) { + switch (c) { + case 'h': + return "help"; + case 'o': + return "output"; + case 'r': + return "reverse"; + case 'i': + return "inverted"; + case 'I': + return "include"; + case 'X': + return "exclude"; + case 't': + return "threads"; + case 's': + return "state"; + default: + return String.valueOf(c); + } + } + + // Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S + private static long parseTimestamp(String time) { + if (time.indexOf(':') < 0) { + return Long.parseLong(time); + } + + GregorianCalendar cal = new GregorianCalendar(); + StringTokenizer st = new StringTokenizer(time, "-:.T"); + + if (time.indexOf('T') > 0) { + cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken())); + cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1); + cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken())); + } + cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + + return cal.getTimeInMillis(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java new file mode 100644 index 00000000000..a75807b5a7a --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java @@ -0,0 +1,32 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.util.Arrays; + +public class CallStack { + String[] names = new String[16]; + byte[] types = new byte[16]; + int size; + + public void push(String name, byte type) { + if (size >= names.length) { + names = Arrays.copyOf(names, size * 2); + types = Arrays.copyOf(types, size * 2); + } + names[size] = name; + types[size] = type; + size++; + } + + public void pop() { + size--; + } + + public void clear() { + size = 0; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java new file mode 100644 index 00000000000..71f106c0c4b --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java @@ -0,0 +1,146 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import io.sentry.protocol.jfr.jfr.StackTrace; + +import static io.sentry.protocol.jfr.convert.Frame.*; + +abstract class Classifier { + + enum Category { + GC("[gc]", TYPE_CPP), + JIT("[jit]", TYPE_CPP), + VM("[vm]", TYPE_CPP), + VTABLE_STUBS("[vtable_stubs]", TYPE_NATIVE), + NATIVE("[native]", TYPE_NATIVE), + INTERPRETER("[Interpreter]", TYPE_NATIVE), + C1_COMP("[c1_comp]", TYPE_C1_COMPILED), + C2_COMP("[c2_comp]", TYPE_INLINED), + ADAPTER("[c2i_adapter]", TYPE_INLINED), + CLASS_INIT("[class_init]", TYPE_CPP), + CLASS_LOAD("[class_load]", TYPE_CPP), + CLASS_RESOLVE("[class_resolve]", TYPE_CPP), + CLASS_VERIFY("[class_verify]", TYPE_CPP), + LAMBDA_INIT("[lambda_init]", TYPE_CPP); + + final String title; + final byte type; + + Category(String title, byte type) { + this.title = title; + this.type = type; + } + } + + public Category getCategory(StackTrace stackTrace) { + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; + + Category category; + if ((category = detectGcJit(methods, types)) == null && + (category = detectClassLoading(methods, types)) == null) { + category = detectOther(methods, types); + } + return category; + } + + private Category detectGcJit(long[] methods, byte[] types) { + boolean vmThread = false; + for (int i = types.length; --i >= 0; ) { + if (types[i] == TYPE_CPP) { + switch (getMethodName(methods[i], types[i])) { + case "CompileBroker::compiler_thread_loop": + return Category.JIT; + case "GCTaskThread::run": + case "WorkerThread::run": + return Category.GC; + case "java_start": + case "thread_native_entry": + vmThread = true; + break; + } + } else if (types[i] != TYPE_NATIVE) { + break; + } + } + return vmThread ? Category.VM : null; + } + + private Category detectClassLoading(long[] methods, byte[] types) { + for (int i = 0; i < methods.length; i++) { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.equals("Verifier::verify")) { + return Category.CLASS_VERIFY; + } else if (methodName.startsWith("InstanceKlass::initialize")) { + return Category.CLASS_INIT; + } else if (methodName.startsWith("LinkResolver::") || + methodName.startsWith("InterpreterRuntime::resolve") || + methodName.startsWith("SystemDictionary::resolve")) { + return Category.CLASS_RESOLVE; + } else if (methodName.endsWith("ClassLoader.loadClass")) { + return Category.CLASS_LOAD; + } else if (methodName.endsWith("LambdaMetafactory.metafactory") || + methodName.endsWith("LambdaMetafactory.altMetafactory")) { + return Category.LAMBDA_INIT; + } else if (methodName.endsWith("table stub")) { + return Category.VTABLE_STUBS; + } else if (methodName.equals("Interpreter")) { + return Category.INTERPRETER; + } else if (methodName.startsWith("I2C/C2I")) { + return i + 1 < types.length && types[i + 1] == TYPE_INTERPRETED ? Category.INTERPRETER : Category.ADAPTER; + } + } + return null; + } + + private Category detectOther(long[] methods, byte[] types) { + boolean inJava = true; + for (int i = 0; i < types.length; i++) { + switch (types[i]) { + case TYPE_INTERPRETED: + return inJava ? Category.INTERPRETER : Category.NATIVE; + case TYPE_JIT_COMPILED: + return inJava ? Category.C2_COMP : Category.NATIVE; + case TYPE_INLINED: + inJava = true; + break; + case TYPE_NATIVE: { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("JVM_") || methodName.startsWith("Unsafe_") || + methodName.startsWith("MHN_") || methodName.startsWith("jni_")) { + return Category.VM; + } + switch (methodName) { + case "call_stub": + case "deoptimization": + case "unknown_Java": + case "not_walkable_Java": + case "InlineCacheBuffer": + return Category.VM; + } + if (methodName.endsWith("_arraycopy") || methodName.contains("pthread_cond")) { + break; + } + inJava = false; + break; + } + case TYPE_CPP: { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("Runtime1::")) { + return Category.C1_COMP; + } + break; + } + case TYPE_C1_COMPILED: + return inJava ? Category.C1_COMP : Category.NATIVE; + } + } + return Category.NATIVE; + } + + protected abstract String getMethodName(long method, byte type); +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java new file mode 100644 index 00000000000..1d662019f99 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java @@ -0,0 +1,395 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +import static io.sentry.protocol.jfr.convert.Frame.*; +import static io.sentry.protocol.jfr.convert.ResourceProcessor.*; + +public class FlameGraph implements Comparator { + private static final Frame[] EMPTY_FRAME_ARRAY = {}; + private static final String[] FRAME_SUFFIX = {"_[0]", "_[j]", "_[i]", "", "", "_[k]", "_[1]"}; + private static final byte HAS_SUFFIX = (byte) 0x80; + private static final int FLUSH_THRESHOLD = 15000; + + private final Arguments args; + private final Index cpool = new Index<>(String.class, ""); + private final Frame root = new Frame(0, TYPE_NATIVE); + private final StringBuilder outbuf = new StringBuilder(FLUSH_THRESHOLD + 1000); + private int[] order; + private int depth; + private int lastLevel; + private long lastX; + private long lastTotal; + private long mintotal; + + public FlameGraph(Arguments args) { + this.args = args; + } + + public void parseCollapsed(Reader in) throws IOException { + CallStack stack = new CallStack(); + + try (BufferedReader br = new BufferedReader(in)) { + for (String line; (line = br.readLine()) != null; ) { + int space = line.lastIndexOf(' '); + if (space <= 0) continue; + + long ticks = Long.parseLong(line.substring(space + 1)); + + for (int from = 0, to; from < space; from = to + 1) { + if ((to = line.indexOf(';', from)) < 0) to = space; + String name = line.substring(from, to); + byte type = detectType(name); + if ((type & HAS_SUFFIX) != 0) { + name = name.substring(0, name.length() - 4); + type ^= HAS_SUFFIX; + } + stack.push(name, type); + } + + addSample(stack, ticks); + stack.clear(); + } + } + } + + public void parseHtml(Reader in) throws IOException { + Frame[] levels = new Frame[128]; + int level = 0; + long total = 0; + boolean needRebuild = args.reverse || args.include != null || args.exclude != null; + + try (BufferedReader br = new BufferedReader(in)) { + while (!br.readLine().startsWith("const cpool")) ; + br.readLine(); + + String s = ""; + for (String line; (line = br.readLine()).startsWith("'"); ) { + String packed = unescape(line.substring(1, line.lastIndexOf('\''))); + s = s.substring(0, packed.charAt(0) - ' ').concat(packed.substring(1)); + cpool.put(s, cpool.size()); + } + + while (!br.readLine().isEmpty()) ; + + for (String line; !(line = br.readLine()).isEmpty(); ) { + StringTokenizer st = new StringTokenizer(line.substring(2, line.length() - 1), ","); + int nameAndType = Integer.parseInt(st.nextToken()); + + char func = line.charAt(0); + if (func == 'f') { + level = Integer.parseInt(st.nextToken()); + st.nextToken(); + } else if (func == 'u') { + level++; + } else if (func != 'n') { + throw new IllegalStateException("Unexpected line: " + line); + } + + if (st.hasMoreTokens()) { + total = Long.parseLong(st.nextToken()); + } + + int titleIndex = nameAndType >>> 3; + byte type = (byte) (nameAndType & 7); + if (st.hasMoreTokens() && (type <= TYPE_INLINED || type >= TYPE_C1_COMPILED)) { + type = TYPE_JIT_COMPILED; + } + + Frame f = level > 0 || needRebuild ? new Frame(titleIndex, type) : root; + f.self = f.total = total; + if (st.hasMoreTokens()) f.inlined = Long.parseLong(st.nextToken()); + if (st.hasMoreTokens()) f.c1 = Long.parseLong(st.nextToken()); + if (st.hasMoreTokens()) f.interpreted = Long.parseLong(st.nextToken()); + + if (level > 0) { + Frame parent = levels[level - 1]; + parent.put(f.key, f); + parent.self -= total; + depth = Math.max(depth, level); + } + if (level >= levels.length) { + levels = Arrays.copyOf(levels, level * 2); + } + levels[level] = f; + } + } + + if (needRebuild) { + rebuild(levels[0], new CallStack(), cpool.keys()); + } + } + + private void rebuild(Frame frame, CallStack stack, String[] strings) { + if (frame.self > 0) { + addSample(stack, frame.self); + } + if (!frame.isEmpty()) { + for (Frame child : frame.values()) { + stack.push(strings[child.getTitleIndex()], child.getType()); + rebuild(child, stack, strings); + stack.pop(); + } + } + } + + public void addSample(CallStack stack, long ticks) { + if (excludeStack(stack)) { + return; + } + + Frame frame = root; + if (args.reverse) { + for (int i = stack.size; --i >= args.skip; ) { + frame = addChild(frame, stack.names[i], stack.types[i], ticks); + } + } else { + for (int i = args.skip; i < stack.size; i++) { + frame = addChild(frame, stack.names[i], stack.types[i], ticks); + } + } + frame.total += ticks; + frame.self += ticks; + + depth = Math.max(depth, stack.size); + } + + public void dump(PrintStream out) { + mintotal = (long) (root.total * args.minwidth / 100); + + if ("collapsed".equals(args.output)) { + printFrameCollapsed(out, root, cpool.keys()); + return; + } + + String tail = getResource("/flame.html"); + + tail = printTill(out, tail, "/*height:*/300"); + int depth = mintotal > 1 ? root.depth(mintotal) : this.depth + 1; + out.print(Math.min(depth * 16, 32767)); + + tail = printTill(out, tail, "/*title:*/"); + out.print(args.title); + + // inverted toggles the layout for reversed stacktraces from icicle to flamegraph + // and for default stacktraces from flamegraphs to icicle. + tail = printTill(out, tail, "/*inverted:*/false"); + out.print(args.reverse ^ args.inverted); + + tail = printTill(out, tail, "/*depth:*/0"); + out.print(depth); + + tail = printTill(out, tail, "/*cpool:*/"); + printCpool(out); + + tail = printTill(out, tail, "/*frames:*/"); + printFrame(out, root, 0, 0); + out.print(outbuf); + + tail = printTill(out, tail, "/*highlight:*/"); + out.print(args.highlight != null ? "'" + escape(args.highlight) + "'" : ""); + + out.print(tail); + } + + private void printCpool(PrintStream out) { + String[] strings = cpool.keys(); + Arrays.sort(strings); + out.print("'all'"); + + order = new int[strings.length]; + String s = ""; + for (int i = 1; i < strings.length; i++) { + int prefixLen = Math.min(getCommonPrefix(s, s = strings[i]), 95); + out.print(",\n'" + escape((char) (prefixLen + ' ') + s.substring(prefixLen)) + "'"); + order[cpool.get(s)] = i; + } + + // cpool is not used beyond this point + cpool.clear(); + } + + private void printFrame(PrintStream out, Frame frame, int level, long x) { + int nameAndType = order[frame.getTitleIndex()] << 3 | frame.getType(); + boolean hasExtraTypes = (frame.inlined | frame.c1 | frame.interpreted) != 0 && + frame.inlined < frame.total && frame.interpreted < frame.total; + + char func = 'f'; + if (level == lastLevel + 1 && x == lastX) { + func = 'u'; + } else if (level == lastLevel && x == lastX + lastTotal) { + func = 'n'; + } + + StringBuilder sb = outbuf.append(func).append('(').append(nameAndType); + if (func == 'f') { + sb.append(',').append(level).append(',').append(x - lastX); + } + if (frame.total != lastTotal || hasExtraTypes) { + sb.append(',').append(frame.total); + if (hasExtraTypes) { + sb.append(',').append(frame.inlined).append(',').append(frame.c1).append(',').append(frame.interpreted); + } + } + sb.append(")\n"); + + if (sb.length() > FLUSH_THRESHOLD) { + out.print(sb); + sb.setLength(0); + } + + lastLevel = level; + lastX = x; + lastTotal = frame.total; + + Frame[] children = frame.values().toArray(EMPTY_FRAME_ARRAY); + Arrays.sort(children, this); + + x += frame.self; + for (Frame child : children) { + if (child.total >= mintotal) { + printFrame(out, child, level + 1, x); + } + x += child.total; + } + } + + private void printFrameCollapsed(PrintStream out, Frame frame, String[] strings) { + StringBuilder sb = outbuf; + int prevLength = sb.length(); + + if (frame != root) { + sb.append(strings[frame.getTitleIndex()]).append(FRAME_SUFFIX[frame.getType()]); + if (frame.self > 0) { + int tmpLength = sb.length(); + out.print(sb.append(' ').append(frame.self).append('\n')); + sb.setLength(tmpLength); + } + sb.append(';'); + } + + if (!frame.isEmpty()) { + for (Frame child : frame.values()) { + if (child.total >= mintotal) { + printFrameCollapsed(out, child, strings); + } + } + } + + sb.setLength(prevLength); + } + + private boolean excludeStack(CallStack stack) { + Pattern include = args.include; + Pattern exclude = args.exclude; + if (include == null && exclude == null) { + return false; + } + + for (int i = 0; i < stack.size; i++) { + if (exclude != null && exclude.matcher(stack.names[i]).matches()) { + return true; + } + if (include != null && include.matcher(stack.names[i]).matches()) { + if (exclude == null) return false; + include = null; + } + } + + return include != null; + } + + private Frame addChild(Frame frame, String title, byte type, long ticks) { + frame.total += ticks; + + int titleIndex = cpool.index(title); + + Frame child; + switch (type) { + case TYPE_INTERPRETED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).interpreted += ticks; + break; + case TYPE_INLINED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).inlined += ticks; + break; + case TYPE_C1_COMPILED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).c1 += ticks; + break; + default: + child = frame.getChild(titleIndex, type); + } + return child; + } + + private static byte detectType(String title) { + if (title.endsWith("_[j]")) { + return TYPE_JIT_COMPILED | HAS_SUFFIX; + } else if (title.endsWith("_[i]")) { + return TYPE_INLINED | HAS_SUFFIX; + } else if (title.endsWith("_[k]")) { + return TYPE_KERNEL | HAS_SUFFIX; + } else if (title.endsWith("_[0]")) { + return TYPE_INTERPRETED | HAS_SUFFIX; + } else if (title.endsWith("_[1]")) { + return TYPE_C1_COMPILED | HAS_SUFFIX; + } else if (title.contains("::") || title.startsWith("-[") || title.startsWith("+[")) { + return TYPE_CPP; + } else if (title.indexOf('/') > 0 && title.charAt(0) != '[' + || title.indexOf('.') > 0 && Character.isUpperCase(title.charAt(0))) { + return TYPE_JIT_COMPILED; + } else { + return TYPE_NATIVE; + } + } + + private static int getCommonPrefix(String a, String b) { + int length = Math.min(a.length(), b.length()); + for (int i = 0; i < length; i++) { + if (a.charAt(i) != b.charAt(i) || a.charAt(i) > 127) { + return i; + } + } + return length; + } + + private static String escape(String s) { + if (s.indexOf('\\') >= 0) s = s.replace("\\", "\\\\"); + if (s.indexOf('\'') >= 0) s = s.replace("'", "\\'"); + return s; + } + + private static String unescape(String s) { + if (s.indexOf('\'') >= 0) s = s.replace("\\'", "'"); + if (s.indexOf('\\') >= 0) s = s.replace("\\\\", "\\"); + return s; + } + + @Override + public int compare(Frame f1, Frame f2) { + return order[f1.getTitleIndex()] - order[f2.getTitleIndex()]; + } + + public static void convert(String input, String output, Arguments args) throws IOException { + FlameGraph fg = new FlameGraph(args); + try (InputStreamReader in = new InputStreamReader(new FileInputStream(input), StandardCharsets.UTF_8)) { + if (input.endsWith(".html")) { + fg.parseHtml(in); + } else { + fg.parseCollapsed(in); + } + } + try (PrintStream out = new PrintStream(output, "UTF-8")) { + fg.dump(out); + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java new file mode 100644 index 00000000000..8cac02b5ca6 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java @@ -0,0 +1,65 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.util.HashMap; + +public class Frame extends HashMap { + public static final byte TYPE_INTERPRETED = 0; + public static final byte TYPE_JIT_COMPILED = 1; + public static final byte TYPE_INLINED = 2; + public static final byte TYPE_NATIVE = 3; + public static final byte TYPE_CPP = 4; + public static final byte TYPE_KERNEL = 5; + public static final byte TYPE_C1_COMPILED = 6; + + private static final int TYPE_SHIFT = 28; + + final int key; + long total; + long self; + long inlined, c1, interpreted; + + private Frame(int key) { + this.key = key; + } + + Frame(int titleIndex, byte type) { + this(titleIndex | type << TYPE_SHIFT); + } + + Frame getChild(int titleIndex, byte type) { + return super.computeIfAbsent(titleIndex | type << TYPE_SHIFT, Frame::new); + } + + int getTitleIndex() { + return key & ((1 << TYPE_SHIFT) - 1); + } + + byte getType() { + if (inlined * 3 >= total) { + return TYPE_INLINED; + } else if (c1 * 2 >= total) { + return TYPE_C1_COMPILED; + } else if (interpreted * 2 >= total) { + return TYPE_INTERPRETED; + } else { + return (byte) (key >>> TYPE_SHIFT); + } + } + + int depth(long cutoff) { + int depth = 0; + if (size() > 0) { + for (Frame child : values()) { + if (child.total >= cutoff) { + depth = Math.max(depth, child.depth(cutoff)); + } + } + } + return depth + 1; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java new file mode 100644 index 00000000000..b0f93b242d4 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java @@ -0,0 +1,47 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.lang.reflect.Array; +import java.util.HashMap; + +public class Index extends HashMap { + private final Class cls; + + public Index(Class cls, T empty) { + this(cls, empty, 256); + } + + public Index(Class cls, T empty, int initialCapacity) { + super(initialCapacity); + this.cls = cls; + super.put(empty, 0); + } + + public int index(T key) { + Integer index = super.get(key); + if (index != null) { + return index; + } else { + int newIndex = super.size(); + super.put(key, newIndex); + return newIndex; + } + } + + @SuppressWarnings("unchecked") + public T[] keys() { + T[] result = (T[]) Array.newInstance(cls, size()); + keys(result); + return result; + } + + public void keys(T[] result) { + for (Entry entry : entrySet()) { + result[entry.getValue()] = entry.getKey(); + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java new file mode 100644 index 00000000000..1860827478e --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java @@ -0,0 +1,275 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import io.sentry.protocol.jfr.jfr.ClassRef; +import io.sentry.protocol.jfr.jfr.Dictionary; +import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.protocol.jfr.jfr.MethodRef; +import io.sentry.protocol.jfr.jfr.event.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; +import java.util.Map; + +import static io.sentry.protocol.jfr.convert.Frame.*; + +public abstract class JfrConverter extends Classifier { + protected final JfrReader jfr; + protected final Arguments args; + protected final EventCollector collector; + protected Dictionary methodNames; + + public JfrConverter(JfrReader jfr, Arguments args) { + this.jfr = jfr; + this.args = args; + + EventCollector collector = createCollector(args); + this.collector = args.nativemem && args.leak ? new MallocLeakAggregator(collector) : collector; + } + + public void convert() throws IOException { + jfr.stopAtNewChunk = true; + + while (jfr.hasMoreChunks()) { + // Reset method dictionary, since new chunk may have different IDs + methodNames = new Dictionary<>(); + + collector.beforeChunk(); + collectEvents(); + collector.afterChunk(); + + convertChunk(); + } + + if (collector.finish()) { + convertChunk(); + } + } + + protected EventCollector createCollector(Arguments args) { + return new EventAggregator(args.threads, args.grain); + } + + protected void collectEvents() throws IOException { + Class eventClass = args.nativemem ? MallocEvent.class + : args.live ? LiveObject.class + : args.alloc ? AllocationSample.class + : args.lock ? ContendedLock.class + : ExecutionSample.class; + + BitSet threadStates = null; + if (args.state != null) { + threadStates = new BitSet(); + for (String state : args.state.toUpperCase().split(",")) { + threadStates.set(toThreadState(state)); + } + } else if (args.cpu) { + threadStates = getThreadStates(true); + } else if (args.wall) { + threadStates = getThreadStates(false); + } + + long startTicks = args.from != 0 ? toTicks(args.from) : Long.MIN_VALUE; + long endTicks = args.to != 0 ? toTicks(args.to) : Long.MAX_VALUE; + + for (Event event; (event = jfr.readEvent(eventClass)) != null; ) { + if (event.time >= startTicks && event.time <= endTicks) { + if (threadStates == null || threadStates.get(((ExecutionSample) event).threadState)) { + collector.collect(event); + } + } + } + } + + protected void convertChunk() { + // To be overridden in subclasses + } + + protected int toThreadState(String name) { + Map threadStates = jfr.enums.get("jdk.types.ThreadState"); + if (threadStates != null) { + for (Map.Entry entry : threadStates.entrySet()) { + if (entry.getValue().startsWith(name, 6)) { + return entry.getKey(); + } + } + } + throw new IllegalArgumentException("Unknown thread state: " + name); + } + + protected BitSet getThreadStates(boolean cpu) { + BitSet set = new BitSet(); + Map threadStates = jfr.enums.get("jdk.types.ThreadState"); + if (threadStates != null) { + for (Map.Entry entry : threadStates.entrySet()) { + set.set(entry.getKey(), "STATE_DEFAULT".equals(entry.getValue()) == cpu); + } + } + return set; + } + + // millis can be an absolute timestamp or an offset from the beginning/end of the recording + protected long toTicks(long millis) { + long nanos = millis * 1_000_000; + if (millis < 0) { + nanos += jfr.endNanos; + } else if (millis < 1500000000000L) { + nanos += jfr.startNanos; + } + return (long) ((nanos - jfr.chunkStartNanos) * (jfr.ticksPerSec / 1e9)) + jfr.chunkStartTicks; + } + + @Override + public String getMethodName(long methodId, byte methodType) { + String result = methodNames.get(methodId); + if (result == null) { + methodNames.put(methodId, result = resolveMethodName(methodId, methodType)); + } + return result; + } + + private String resolveMethodName(long methodId, byte methodType) { + MethodRef method = jfr.methods.get(methodId); + if (method == null) { + return "unknown"; + } + + ClassRef cls = jfr.classes.get(method.cls); + byte[] className = jfr.symbols.get(cls.name); + byte[] methodName = jfr.symbols.get(method.name); + + if (className == null || className.length == 0 || isNativeFrame(methodType)) { + return new String(methodName, StandardCharsets.UTF_8); + } else { + String classStr = toJavaClassName(className, 0, args.dot); + if (methodName == null || methodName.length == 0) { + return classStr; + } + String methodStr = new String(methodName, StandardCharsets.UTF_8); + return classStr + '.' + methodStr; + } + } + + public String getClassName(long classId) { + ClassRef cls = jfr.classes.get(classId); + if (cls == null) { + return "null"; + } + byte[] className = jfr.symbols.get(cls.name); + + int arrayDepth = 0; + while (className[arrayDepth] == '[') { + arrayDepth++; + } + + String name = toJavaClassName(className, arrayDepth, true); + while (arrayDepth-- > 0) { + name = name.concat("[]"); + } + return name; + } + + private String toJavaClassName(byte[] symbol, int start, boolean dotted) { + int end = symbol.length; + if (start > 0) { + switch (symbol[start]) { + case 'B': + return "byte"; + case 'C': + return "char"; + case 'S': + return "short"; + case 'I': + return "int"; + case 'J': + return "long"; + case 'Z': + return "boolean"; + case 'F': + return "float"; + case 'D': + return "double"; + case 'L': + start++; + end--; + } + } + + if (args.norm) { + for (int i = end - 2; i > start; i--) { + if (symbol[i] == '/' || symbol[i] == '.') { + if (symbol[i + 1] >= '0' && symbol[i + 1] <= '9') { + end = i; + if (i > start + 19 && symbol[i - 19] == '+' && symbol[i - 18] == '0') { + // Original JFR transforms lambda names to something like + // pkg.ClassName$$Lambda+0x00007f8177090218/543846639 + end = i - 19; + } + } + break; + } + } + } + + if (args.simple) { + for (int i = end - 2; i >= start; i--) { + if (symbol[i] == '/' && (symbol[i + 1] < '0' || symbol[i + 1] > '9')) { + start = i + 1; + break; + } + } + } + + String s = new String(symbol, start, end - start, StandardCharsets.UTF_8); + return dotted ? s.replace('/', '.') : s; + } + + public StackTraceElement getStackTraceElement(long methodId, byte methodType, int location) { + MethodRef method = jfr.methods.get(methodId); + if (method == null) { + return new StackTraceElement("", "unknown", null, 0); + } + + ClassRef cls = jfr.classes.get(method.cls); + byte[] className = jfr.symbols.get(cls.name); + byte[] methodName = jfr.symbols.get(method.name); + + String classStr = className == null || className.length == 0 || isNativeFrame(methodType) ? "" : + toJavaClassName(className, 0, args.dot); + String methodStr = methodName == null || methodName.length == 0 ? "" : + new String(methodName, StandardCharsets.UTF_8); + return new StackTraceElement(classStr, methodStr, null, location >>> 16); + } + + public String getThreadName(int tid) { + String threadName = jfr.threads.get(tid); + return threadName == null ? "[tid=" + tid + ']' : + threadName.startsWith("[tid=") ? threadName : '[' + threadName + " tid=" + tid + ']'; + } + + protected boolean isNativeFrame(byte methodType) { + // In JDK Flight Recorder, TYPE_NATIVE denotes Java native methods, + // while in async-profiler, TYPE_NATIVE is for C methods + return methodType == TYPE_NATIVE && jfr.getEnumValue("jdk.types.FrameType", TYPE_KERNEL) != null || + methodType == TYPE_CPP || + methodType == TYPE_KERNEL; + } + + // Select sum(samples) or sum(value) depending on the --total option. + // For lock events, convert lock duration from ticks to nanoseconds. + protected abstract class AggregatedEventVisitor implements EventCollector.Visitor { + final double factor = !args.total ? 0.0 : args.lock ? 1e9 / jfr.ticksPerSec : 1.0; + + @Override + public final void visit(Event event, long samples, long value) { + visit(event, factor == 0.0 ? samples : factor == 1.0 ? value : (long) (value * factor)); + } + + protected abstract void visit(Event event, long value); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java new file mode 100644 index 00000000000..469f0979aed --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java @@ -0,0 +1,91 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.protocol.jfr.jfr.StackTrace; +import io.sentry.protocol.jfr.jfr.event.AllocationSample; +import io.sentry.protocol.jfr.jfr.event.Event; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import static io.sentry.protocol.jfr.convert.Frame.*; + +/** + * Converts .jfr output to HTML Flame Graph. + */ +public class JfrToFlame extends JfrConverter { + private final FlameGraph fg; + + public JfrToFlame(JfrReader jfr, Arguments args) { + super(jfr, args); + this.fg = new FlameGraph(args); + } + + @Override + protected void convertChunk() { + collector.forEach(new AggregatedEventVisitor() { + final CallStack stack = new CallStack(); + + @Override + public void visit(Event event, long value) { + StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + if (stackTrace != null) { + Arguments args = JfrToFlame.this.args; + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; + int[] locations = stackTrace.locations; + + if (args.threads) { + stack.push(getThreadName(event.tid), TYPE_NATIVE); + } + if (args.classify) { + Classifier.Category category = getCategory(stackTrace); + stack.push(category.title, category.type); + } + for (int i = methods.length; --i >= 0; ) { + String methodName = getMethodName(methods[i], types[i]); + int location; + if (args.lines && (location = locations[i] >>> 16) != 0) { + methodName += ":" + location; + } else if (args.bci && (location = locations[i] & 0xffff) != 0) { + methodName += "@" + location; + } + stack.push(methodName, types[i]); + } + long classId = event.classId(); + if (classId != 0) { + stack.push(getClassName(classId), (event instanceof AllocationSample) + && ((AllocationSample) event).tlabSize == 0 ? TYPE_KERNEL : TYPE_INLINED); + } + + fg.addSample(stack, value); + stack.clear(); + } + } + }); + } + + public void dump(OutputStream out) throws IOException { + try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { + fg.dump(ps); + } + } + + public static void convert(String input, String output, Arguments args) throws IOException { + JfrToFlame converter; + try (JfrReader jfr = new JfrReader(input)) { + converter = new JfrToFlame(jfr, args); + converter.convert(); + } + try (FileOutputStream out = new FileOutputStream(output)) { + converter.dump(out); + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java new file mode 100644 index 00000000000..d26ba5ae736 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java @@ -0,0 +1,96 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import one.heatmap.Heatmap; +import one.jfr.Dictionary; +import one.jfr.JfrReader; +import one.jfr.StackTrace; +import one.jfr.event.AllocationSample; +import one.jfr.event.ContendedLock; +import one.jfr.event.Event; +import one.jfr.event.EventCollector; + +import java.io.*; + +import static one.convert.Frame.TYPE_INLINED; +import static one.convert.Frame.TYPE_KERNEL; + +public class JfrToHeatmap extends JfrConverter { + private final Heatmap heatmap; + + public JfrToHeatmap(JfrReader jfr, Arguments args) { + super(jfr, args); + this.heatmap = new Heatmap(args, this); + } + + @Override + protected EventCollector createCollector(Arguments args) { + return new EventCollector() { + @Override + public void collect(Event event) { + int extra = 0; + byte type = 0; + if (event instanceof AllocationSample) { + extra = ((AllocationSample) event).classId; + type = ((AllocationSample) event).tlabSize == 0 ? TYPE_KERNEL : TYPE_INLINED; + } else if (event instanceof ContendedLock) { + extra = ((ContendedLock) event).classId; + type = TYPE_KERNEL; + } + + long msFromStart = (event.time - jfr.chunkStartTicks) * 1_000 / jfr.ticksPerSec; + long timeMs = jfr.chunkStartNanos / 1_000_000 + msFromStart; + + heatmap.addEvent(event.stackTraceId, extra, type, timeMs); + } + + @Override + public void beforeChunk() { + heatmap.beforeChunk(); + jfr.stackTraces.forEach(new Dictionary.Visitor() { + @Override + public void visit(long key, StackTrace trace) { + heatmap.addStack(key, trace.methods, trace.locations, trace.types, trace.methods.length); + } + }); + } + + @Override + public void afterChunk() { + jfr.stackTraces.clear(); + } + + @Override + public boolean finish() { + heatmap.finish(jfr.startNanos / 1_000_000); + return false; + } + + @Override + public void forEach(Visitor visitor) { + throw new AssertionError("Should not be called"); + } + }; + } + + public void dump(OutputStream out) throws IOException { + try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { + heatmap.dump(ps); + } + } + + public static void convert(String input, String output, Arguments args) throws IOException { + JfrToHeatmap converter; + try (JfrReader jfr = new JfrReader(input)) { + converter = new JfrToHeatmap(jfr, args); + converter.convert(); + } + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(output))) { + converter.dump(out); + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java new file mode 100644 index 00000000000..b6f08ac0a81 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.convert; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +public class ResourceProcessor { + + public static String getResource(String name) { + try (InputStream stream = ResourceProcessor.class.getResourceAsStream(name)) { + if (stream == null) { + throw new IOException("No resource found"); + } + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[32768]; + for (int length; (length = stream.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + return result.toString("UTF-8"); + } catch (IOException e) { + throw new IllegalStateException("Can't load resource with name " + name); + } + } + + public static String printTill(PrintStream out, String data, String till) { + int index = data.indexOf(till); + out.print(data.substring(0, index)); + return data.substring(index + till.length()); + } + +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java new file mode 100644 index 00000000000..6367830edce --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java @@ -0,0 +1,14 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +public class ClassRef { + public final long name; + + public ClassRef(long name) { + this.name = name; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java new file mode 100644 index 00000000000..c903a69e684 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java @@ -0,0 +1,116 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +import java.util.Arrays; + +/** + * Fast and compact long->Object map. + */ +public class Dictionary { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private Object[] values; + private int size; + + public Dictionary() { + this(INITIAL_CAPACITY); + } + + public Dictionary(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new Object[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, null); + size = 0; + } + + public int size() { + return size; + } + + public void put(long key, T value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); + } + + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; + } + keys[i] = key; + values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + @SuppressWarnings("unchecked") + public T get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key && keys[i] != 0) { + i = (i + 1) & mask; + } + return (T) values[i]; + } + + @SuppressWarnings("unchecked") + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], (T) values[i]); + } + } + } + + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); + } + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + Object[] newValues = new Object[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + values = newValues; + } + + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } + + public interface Visitor { + void visit(long key, T value); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java new file mode 100644 index 00000000000..aec9b7b624f --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java @@ -0,0 +1,125 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +import java.util.Arrays; + +/** + * Fast and compact long->int map. + */ +public class DictionaryInt { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private int[] values; + private int size; + + public DictionaryInt() { + this(INITIAL_CAPACITY); + } + + public DictionaryInt(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new int[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, 0); + size = 0; + } + + public void put(long key, int value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); + } + + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; + } + keys[i] = key; + values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + public int get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + throw new IllegalArgumentException("No such key: " + key); + } + i = (i + 1) & mask; + } + return values[i]; + } + + public int get(long key, int notFound) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + return notFound; + } + i = (i + 1) & mask; + } + return values[i]; + } + + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], values[i]); + } + } + } + + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); + } + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + int[] newValues = new int[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + values = newValues; + } + + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } + + public interface Visitor { + void visit(long key, int value); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java new file mode 100644 index 00000000000..d814026a84c --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java @@ -0,0 +1,12 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +class Element { + + void addChild(Element e) { + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java new file mode 100644 index 00000000000..fbdbc521357 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java @@ -0,0 +1,40 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class JfrClass extends Element { + final int id; + final boolean simpleType; + final String name; + final List fields; + + JfrClass(Map attributes) { + this.id = Integer.parseInt(attributes.get("id")); + this.simpleType = "true".equals(attributes.get("simpleType")); + this.name = attributes.get("name"); + this.fields = new ArrayList<>(2); + } + + @Override + void addChild(Element e) { + if (e instanceof JfrField) { + fields.add((JfrField) e); + } + } + + public JfrField field(String name) { + for (JfrField field : fields) { + if (field.name.equals(name)) { + return field; + } + } + return null; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java new file mode 100644 index 00000000000..a96f5555e5b --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java @@ -0,0 +1,20 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +import java.util.Map; + +public class JfrField extends Element { + final String name; + final int type; + final boolean constantPool; + + JfrField(Map attributes) { + this.name = attributes.get("name"); + this.type = Integer.parseInt(attributes.get("class")); + this.constantPool = "true".equals(attributes.get("constantPool")); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java new file mode 100644 index 00000000000..5aad97a002d --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java @@ -0,0 +1,685 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +import io.sentry.protocol.jfr.jfr.event.*; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Parses JFR output produced by async-profiler. + */ +public class JfrReader implements Closeable { + private static final int BUFFER_SIZE = 2 * 1024 * 1024; + private static final int CHUNK_HEADER_SIZE = 68; + private static final int CHUNK_SIGNATURE = 0x464c5200; + + private static final byte STATE_NEW_CHUNK = 0; + private static final byte STATE_READING = 1; + private static final byte STATE_EOF = 2; + private static final byte STATE_INCOMPLETE = 3; + + private final FileChannel ch; + private ByteBuffer buf; + private final long fileSize; + private long filePosition; + private byte state; + + public long startNanos = Long.MAX_VALUE; + public long endNanos = Long.MIN_VALUE; + public long startTicks = Long.MAX_VALUE; + public long chunkStartNanos; + public long chunkEndNanos; + public long chunkStartTicks; + public long ticksPerSec; + public boolean stopAtNewChunk; + + public final Dictionary types = new Dictionary<>(); + public final Map typesByName = new HashMap<>(); + public final Dictionary threads = new Dictionary<>(); + public final Dictionary classes = new Dictionary<>(); + public final Dictionary strings = new Dictionary<>(); + public final Dictionary symbols = new Dictionary<>(); + public final Dictionary methods = new Dictionary<>(); + public final Dictionary stackTraces = new Dictionary<>(); + public final Map settings = new HashMap<>(); + public final Map> enums = new HashMap<>(); + + private final Dictionary> customEvents = new Dictionary<>(); + + private int executionSample; + private int nativeMethodSample; + private int wallClockSample; + private int allocationInNewTLAB; + private int allocationOutsideTLAB; + private int allocationSample; + private int liveObject; + private int monitorEnter; + private int threadPark; + private int activeSetting; + private int malloc; + private int free; + + public JfrReader(String fileName) throws IOException { + this.ch = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ); + this.buf = ByteBuffer.allocateDirect(BUFFER_SIZE); + this.fileSize = ch.size(); + + buf.flip(); + ensureBytes(CHUNK_HEADER_SIZE); + if (!readChunk(0)) { + throw new IOException("Incomplete JFR file"); + } + } + + public JfrReader(ByteBuffer buf) throws IOException { + this.ch = null; + this.buf = buf; + this.fileSize = buf.limit(); + + buf.order(ByteOrder.BIG_ENDIAN); + if (!readChunk(0)) { + throw new IOException("Incomplete JFR file"); + } + } + + @Override + public void close() throws IOException { + if (ch != null) { + ch.close(); + } + } + + public boolean eof() { + return state >= STATE_EOF; + } + + public boolean incomplete() { + return state == STATE_INCOMPLETE; + } + + public long durationNanos() { + return endNanos - startNanos; + } + + public void registerEvent(String name, Class eventClass) { + JfrClass type = typesByName.get(name); + if (type != null) { + try { + customEvents.put(type.id, eventClass.getConstructor(JfrReader.class)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("No suitable constructor found"); + } + } + } + + // Similar to eof(), but parses the next chunk header + public boolean hasMoreChunks() throws IOException { + return state == STATE_NEW_CHUNK ? readChunk(buf.position()) : state == STATE_READING; + } + + public List readAllEvents() throws IOException { + return readAllEvents(null); + } + + public List readAllEvents(Class cls) throws IOException { + ArrayList events = new ArrayList<>(); + for (E event; (event = readEvent(cls)) != null; ) { + events.add(event); + } + Collections.sort(events); + return events; + } + + public Event readEvent() throws IOException { + return readEvent(null); + } + + @SuppressWarnings("unchecked") + public E readEvent(Class cls) throws IOException { + while (ensureBytes(CHUNK_HEADER_SIZE)) { + int pos = buf.position(); + int size = getVarint(); + int type = getVarint(); + + if (type == 'L' && buf.getInt(pos) == CHUNK_SIGNATURE) { + if (state != STATE_NEW_CHUNK && stopAtNewChunk) { + buf.position(pos); + state = STATE_NEW_CHUNK; + } else if (readChunk(pos)) { + continue; + } + return null; + } + + if (type == executionSample || type == nativeMethodSample) { + if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(false); + } else if (type == wallClockSample) { + if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(true); + } else if (type == allocationInNewTLAB) { + if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(true); + } else if (type == allocationOutsideTLAB || type == allocationSample) { + if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(false); + } else if (type == malloc) { + if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(true); + } else if (type == free) { + if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(false); + } else if (type == liveObject) { + if (cls == null || cls == LiveObject.class) return (E) readLiveObject(); + } else if (type == monitorEnter) { + if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(false); + } else if (type == threadPark) { + if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(true); + } else if (type == activeSetting) { + readActiveSetting(); + } else { + Constructor customEvent = customEvents.get(type); + if (customEvent != null && (cls == null || cls == customEvent.getDeclaringClass())) { + try { + return (E) customEvent.newInstance(this); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } finally { + seek(filePosition + pos + size); + } + } + } + + seek(filePosition + pos + size); + } + + state = STATE_EOF; + return null; + } + + private ExecutionSample readExecutionSample(boolean hasSamples) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int threadState = getVarint(); + int samples = hasSamples ? getVarint() : 1; + return new ExecutionSample(time, tid, stackTraceId, threadState, samples); + } + + private AllocationSample readAllocationSample(boolean tlab) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + long allocationSize = getVarlong(); + long tlabSize = tlab ? getVarlong() : 0; + return new AllocationSample(time, tid, stackTraceId, classId, allocationSize, tlabSize); + } + + private MallocEvent readMallocEvent(boolean hasSize) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + long address = getVarlong(); + long size = hasSize ? getVarlong() : 0; + return new MallocEvent(time, tid, stackTraceId, address, size); + } + + private LiveObject readLiveObject() { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + long allocationSize = getVarlong(); + long allocatimeTime = getVarlong(); + return new LiveObject(time, tid, stackTraceId, classId, allocationSize, allocatimeTime); + } + + private ContendedLock readContendedLock(boolean hasTimeout) { + long time = getVarlong(); + long duration = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + if (hasTimeout) getVarlong(); + long until = getVarlong(); + long address = getVarlong(); + return new ContendedLock(time, tid, stackTraceId, duration, classId); + } + + private void readActiveSetting() { + for (JfrField field : typesByName.get("jdk.ActiveSetting").fields) { + getVarlong(); + if ("id".equals(field.name)) { + break; + } + } + String name = getString(); + String value = getString(); + settings.put(name, value); + } + + private boolean readChunk(int pos) throws IOException { + if (pos + CHUNK_HEADER_SIZE > buf.limit() || buf.getInt(pos) != CHUNK_SIGNATURE) { + throw new IOException("Not a valid JFR file"); + } + + int version = buf.getInt(pos + 4); + if (version < 0x20000 || version > 0x2ffff) { + throw new IOException("Unsupported JFR version: " + (version >>> 16) + "." + (version & 0xffff)); + } + + long chunkStart = filePosition + pos; + long chunkSize = buf.getLong(pos + 8); + if (chunkStart + chunkSize > fileSize) { + state = STATE_INCOMPLETE; + return false; + } + + long cpOffset = buf.getLong(pos + 16); + long metaOffset = buf.getLong(pos + 24); + if (cpOffset == 0 || metaOffset == 0) { + state = STATE_INCOMPLETE; + return false; + } + + chunkStartNanos = buf.getLong(pos + 32); + chunkEndNanos = buf.getLong(pos + 32) + buf.getLong(pos + 40); + chunkStartTicks = buf.getLong(pos + 48); + ticksPerSec = buf.getLong(pos + 56); + + startNanos = Math.min(startNanos, chunkStartNanos); + endNanos = Math.max(endNanos, chunkEndNanos); + startTicks = Math.min(startTicks, chunkStartTicks); + + types.clear(); + typesByName.clear(); + + readMeta(chunkStart + metaOffset); + readConstantPool(chunkStart + cpOffset); + cacheEventTypes(); + + seek(chunkStart + CHUNK_HEADER_SIZE); + state = STATE_READING; + return true; + } + + private void readMeta(long metaOffset) throws IOException { + seek(metaOffset); + ensureBytes(5); + + int posBeforeSize = buf.position(); + ensureBytes(getVarint() - (buf.position() - posBeforeSize)); + getVarint(); + getVarlong(); + getVarlong(); + getVarlong(); + + String[] strings = new String[getVarint()]; + for (int i = 0; i < strings.length; i++) { + strings[i] = getString(); + } + readElement(strings); + } + + private Element readElement(String[] strings) { + String name = strings[getVarint()]; + + int attributeCount = getVarint(); + Map attributes = new HashMap<>(attributeCount); + for (int i = 0; i < attributeCount; i++) { + attributes.put(strings[getVarint()], strings[getVarint()]); + } + + Element e = createElement(name, attributes); + int childCount = getVarint(); + for (int i = 0; i < childCount; i++) { + e.addChild(readElement(strings)); + } + return e; + } + + private Element createElement(String name, Map attributes) { + switch (name) { + case "class": { + JfrClass type = new JfrClass(attributes); + if (!attributes.containsKey("superType")) { + types.put(type.id, type); + } + typesByName.put(type.name, type); + return type; + } + case "field": + return new JfrField(attributes); + default: + return new Element(); + } + } + + private void readConstantPool(long cpOffset) throws IOException { + long delta; + do { + seek(cpOffset); + ensureBytes(5); + + int posBeforeSize = buf.position(); + ensureBytes(getVarint() - (buf.position() - posBeforeSize)); + getVarint(); + getVarlong(); + getVarlong(); + delta = getVarlong(); + getVarint(); + + int poolCount = getVarint(); + for (int i = 0; i < poolCount; i++) { + int type = getVarint(); + readConstants(types.get(type)); + } + } while (delta != 0 && (cpOffset += delta) > 0); + } + + private void readConstants(JfrClass type) { + switch (type.name) { + case "jdk.types.ChunkHeader": + buf.position(buf.position() + (CHUNK_HEADER_SIZE + 3)); + break; + case "java.lang.Thread": + readThreads(type.fields.size()); + break; + case "java.lang.Class": + readClasses(type.fields.size()); + break; + case "java.lang.String": + readStrings(); + break; + case "jdk.types.Symbol": + readSymbols(); + break; + case "jdk.types.Method": + readMethods(); + break; + case "jdk.types.StackTrace": + readStackTraces(); + break; + default: + if (type.simpleType && type.fields.size() == 1) { + readEnumValues(type.name); + } else { + readOtherConstants(type.fields); + } + } + } + + private void readThreads(int fieldCount) { + int count = threads.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + String osName = getString(); + int osThreadId = getVarint(); + String javaName = getString(); + long javaThreadId = getVarlong(); + readFields(fieldCount - 4); + threads.put(id, javaName != null ? javaName : osName); + } + } + + private void readClasses(int fieldCount) { + int count = classes.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + long loader = getVarlong(); + long name = getVarlong(); + long pkg = getVarlong(); + int modifiers = getVarint(); + readFields(fieldCount - 4); + classes.put(id, new ClassRef(name)); + } + } + + private void readMethods() { + int count = methods.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + long cls = getVarlong(); + long name = getVarlong(); + long sig = getVarlong(); + int modifiers = getVarint(); + int hidden = getVarint(); + methods.put(id, new MethodRef(cls, name, sig)); + } + } + + private void readStackTraces() { + int count = stackTraces.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + int truncated = getVarint(); + StackTrace stackTrace = readStackTrace(); + stackTraces.put(id, stackTrace); + } + } + + private StackTrace readStackTrace() { + int depth = getVarint(); + long[] methods = new long[depth]; + byte[] types = new byte[depth]; + int[] locations = new int[depth]; + for (int i = 0; i < depth; i++) { + methods[i] = getVarlong(); + int line = getVarint(); + int bci = getVarint(); + locations[i] = line << 16 | (bci & 0xffff); + types[i] = buf.get(); + } + return new StackTrace(methods, types, locations); + } + + private void readStrings() { + int count = strings.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + strings.put(getVarlong(), getString()); + } + } + + private void readSymbols() { + int count = symbols.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + if (buf.get() != 3) { + throw new IllegalArgumentException("Invalid symbol encoding"); + } + symbols.put(id, getBytes()); + } + } + + private void readEnumValues(String typeName) { + HashMap map = new HashMap<>(); + int count = getVarint(); + for (int i = 0; i < count; i++) { + map.put((int) getVarlong(), getString()); + } + enums.put(typeName, map); + } + + private void readOtherConstants(List fields) { + int stringType = getTypeId("java.lang.String"); + + boolean[] numeric = new boolean[fields.size()]; + for (int i = 0; i < numeric.length; i++) { + JfrField f = fields.get(i); + numeric[i] = f.constantPool || f.type != stringType; + } + + int count = getVarint(); + for (int i = 0; i < count; i++) { + getVarlong(); + readFields(numeric); + } + } + + private void readFields(boolean[] numeric) { + for (boolean n : numeric) { + if (n) { + getVarlong(); + } else { + getString(); + } + } + } + + private void readFields(int count) { + while (count-- > 0) { + getVarlong(); + } + } + + private void cacheEventTypes() { + executionSample = getTypeId("jdk.ExecutionSample"); + nativeMethodSample = getTypeId("jdk.NativeMethodSample"); + wallClockSample = getTypeId("profiler.WallClockSample"); + allocationInNewTLAB = getTypeId("jdk.ObjectAllocationInNewTLAB"); + allocationOutsideTLAB = getTypeId("jdk.ObjectAllocationOutsideTLAB"); + allocationSample = getTypeId("jdk.ObjectAllocationSample"); + liveObject = getTypeId("profiler.LiveObject"); + monitorEnter = getTypeId("jdk.JavaMonitorEnter"); + threadPark = getTypeId("jdk.ThreadPark"); + activeSetting = getTypeId("jdk.ActiveSetting"); + malloc = getTypeId("profiler.Malloc"); + free = getTypeId("profiler.Free"); + + registerEvent("jdk.CPULoad", CPULoad.class); + registerEvent("jdk.GCHeapSummary", GCHeapSummary.class); + registerEvent("jdk.ObjectCount", ObjectCount.class); + registerEvent("jdk.ObjectCountAfterGC", ObjectCount.class); + } + + private int getTypeId(String typeName) { + JfrClass type = typesByName.get(typeName); + return type != null ? type.id : -1; + } + + public int getEnumKey(String typeName, String value) { + Map enumValues = enums.get(typeName); + if (enumValues != null) { + for (Map.Entry entry : enumValues.entrySet()) { + if (value.equals(entry.getValue())) { + return entry.getKey(); + } + } + } + return -1; + } + + public String getEnumValue(String typeName, int key) { + return enums.get(typeName).get(key); + } + + public int getVarint() { + int result = 0; + for (int shift = 0; ; shift += 7) { + byte b = buf.get(); + result |= (b & 0x7f) << shift; + if (b >= 0) { + return result; + } + } + } + + public long getVarlong() { + long result = 0; + for (int shift = 0; shift < 56; shift += 7) { + byte b = buf.get(); + result |= (b & 0x7fL) << shift; + if (b >= 0) { + return result; + } + } + return result | (buf.get() & 0xffL) << 56; + } + + public float getFloat() { + return buf.getFloat(); + } + + public double getDouble() { + return buf.getDouble(); + } + + public String getString() { + switch (buf.get()) { + case 0: + return null; + case 1: + return ""; + case 2: + return strings.get(getVarlong()); + case 3: + return new String(getBytes(), StandardCharsets.UTF_8); + case 4: { + char[] chars = new char[getVarint()]; + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) getVarint(); + } + return new String(chars); + } + case 5: + return new String(getBytes(), StandardCharsets.ISO_8859_1); + default: + throw new IllegalArgumentException("Invalid string encoding"); + } + } + + public byte[] getBytes() { + byte[] bytes = new byte[getVarint()]; + buf.get(bytes); + return bytes; + } + + private void seek(long pos) throws IOException { + long bufPosition = pos - filePosition; + if (bufPosition >= 0 && bufPosition <= buf.limit()) { + buf.position((int) bufPosition); + } else { + filePosition = pos; + ch.position(pos); + buf.rewind().flip(); + } + } + + private boolean ensureBytes(int needed) throws IOException { + if (buf.remaining() >= needed) { + return true; + } + + if (ch == null) { + return false; + } + + filePosition += buf.position(); + + if (buf.capacity() < needed) { + ByteBuffer newBuf = ByteBuffer.allocateDirect(needed); + newBuf.put(buf); + buf = newBuf; + } else { + buf.compact(); + } + + while (ch.read(buf) > 0 && buf.position() < needed) { + // keep reading + } + buf.flip(); + return buf.limit() > 0; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java new file mode 100644 index 00000000000..79e967783df --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java @@ -0,0 +1,18 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +public class MethodRef { + public final long cls; + public final long name; + public final long sig; + + public MethodRef(long cls, long name, long sig) { + this.cls = cls; + this.name = name; + this.sig = sig; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java new file mode 100644 index 00000000000..519ce407fb2 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java @@ -0,0 +1,18 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr; + +public class StackTrace { + public final long[] methods; + public final byte[] types; + public final int[] locations; + + public StackTrace(long[] methods, byte[] types, int[] locations) { + this.methods = methods; + this.types = types; + this.locations = locations; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java new file mode 100644 index 00000000000..5f0faef7eb6 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java @@ -0,0 +1,43 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class AllocationSample extends Event { + public final int classId; + public final long allocationSize; + public final long tlabSize; + + public AllocationSample(long time, int tid, int stackTraceId, int classId, long allocationSize, long tlabSize) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.tlabSize = tlabSize; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId + (tlabSize == 0 ? 17 : 0); + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof AllocationSample) { + AllocationSample a = (AllocationSample) o; + return classId == a.classId && (tlabSize == 0) == (a.tlabSize == 0); + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return tlabSize != 0 ? tlabSize : allocationSize; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java new file mode 100644 index 00000000000..6a955cf9e23 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java @@ -0,0 +1,21 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +import io.sentry.protocol.jfr.jfr.JfrReader; + +public class CPULoad extends Event { + public final float jvmUser; + public final float jvmSystem; + public final float machineTotal; + + public CPULoad(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.jvmUser = jfr.getFloat(); + this.jvmSystem = jfr.getFloat(); + this.machineTotal = jfr.getFloat(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java new file mode 100644 index 00000000000..bc01e294b83 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java @@ -0,0 +1,41 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class ContendedLock extends Event { + public final long duration; + public final int classId; + + public ContendedLock(long time, int tid, int stackTraceId, long duration, int classId) { + super(time, tid, stackTraceId); + this.duration = duration; + this.classId = classId; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof ContendedLock) { + ContendedLock c = (ContendedLock) o; + return classId == c.classId; + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return duration; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java new file mode 100644 index 00000000000..2493e3eb5fb --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java @@ -0,0 +1,62 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +import java.lang.reflect.Field; + +public abstract class Event implements Comparable { + public final long time; + public final int tid; + public final int stackTraceId; + + protected Event(long time, int tid, int stackTraceId) { + this.time = time; + this.tid = tid; + this.stackTraceId = stackTraceId; + } + + @Override + public int compareTo(Event o) { + return Long.compare(time, o.time); + } + + @Override + public int hashCode() { + return stackTraceId; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()) + .append("{time=").append(time) + .append(",tid=").append(tid) + .append(",stackTraceId=").append(stackTraceId); + for (Field f : getClass().getDeclaredFields()) { + try { + sb.append(',').append(f.getName()).append('=').append(f.get(this)); + } catch (ReflectiveOperationException e) { + break; + } + } + return sb.append('}').toString(); + } + + public boolean sameGroup(Event o) { + return getClass() == o.getClass(); + } + + public long classId() { + return 0; + } + + public long samples() { + return 1; + } + + public long value() { + return 1; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java new file mode 100644 index 00000000000..00bccf89203 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java @@ -0,0 +1,149 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class EventAggregator implements EventCollector { + private static final int INITIAL_CAPACITY = 1024; + + private final boolean threads; + private final double grain; + private Event[] keys; + private long[] samples; + private long[] values; + private int size; + private double fraction; + + public EventAggregator(boolean threads, double grain) { + this.threads = threads; + this.grain = grain; + + beforeChunk(); + } + + public int size() { + return size; + } + + @Override + public void collect(Event e) { + collect(e, e.samples(), e.value()); + } + + public void collect(Event e, long samples, long value) { + int mask = keys.length - 1; + int i = hashCode(e) & mask; + while (keys[i] != null) { + if (sameGroup(keys[i], e)) { + this.samples[i] += samples; + this.values[i] += value; + return; + } + i = (i + 1) & mask; + } + + this.keys[i] = e; + this.samples[i] = samples; + this.values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + @Override + public void beforeChunk() { + if (keys == null || size > 0) { + keys = new Event[INITIAL_CAPACITY]; + samples = new long[INITIAL_CAPACITY]; + values = new long[INITIAL_CAPACITY]; + size = 0; + } + } + + @Override + public void afterChunk() { + if (grain > 0) { + coarsen(grain); + } + } + + @Override + public boolean finish() { + keys = null; + samples = null; + values = null; + return false; + } + + @Override + public void forEach(Visitor visitor) { + if (size > 0) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + visitor.visit(keys[i], samples[i], values[i]); + } + } + } + } + + public void coarsen(double grain) { + fraction = 0; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + long s0 = samples[i]; + long s1 = round(s0 / grain); + if (s1 == 0) { + keys[i] = null; + size--; + } + samples[i] = s1; + values[i] = (long) (values[i] * ((double) s1 / s0)); + } + } + } + + private long round(double d) { + long r = (long) d; + if ((fraction += d - r) >= 1.0) { + fraction -= 1.0; + r++; + } + return r; + } + + private int hashCode(Event e) { + return e.hashCode() + (threads ? e.tid * 31 : 0); + } + + private boolean sameGroup(Event e1, Event e2) { + return e1.stackTraceId == e2.stackTraceId && (!threads || e1.tid == e2.tid) && e1.sameGroup(e2); + } + + private void resize(int newCapacity) { + Event[] newKeys = new Event[newCapacity]; + long[] newSamples = new long[newCapacity]; + long[] newValues = new long[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == null) { + newKeys[j] = keys[i]; + newSamples[j] = samples[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + samples = newSamples; + values = newValues; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java new file mode 100644 index 00000000000..b35fc0a2c71 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java @@ -0,0 +1,24 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public interface EventCollector { + + void collect(Event e); + + void beforeChunk(); + + void afterChunk(); + + // Returns true if this collector has remaining data to process + boolean finish(); + + void forEach(Visitor visitor); + + interface Visitor { + void visit(Event event, long samples, long value); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java new file mode 100644 index 00000000000..8b8b2cbb3cb --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java @@ -0,0 +1,27 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class ExecutionSample extends Event { + public final int threadState; + public final int samples; + + public ExecutionSample(long time, int tid, int stackTraceId, int threadState, int samples) { + super(time, tid, stackTraceId); + this.threadState = threadState; + this.samples = samples; + } + + @Override + public long samples() { + return samples; + } + + @Override + public long value() { + return samples; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java new file mode 100644 index 00000000000..6f4ca0b7466 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java @@ -0,0 +1,28 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +import io.sentry.protocol.jfr.jfr.JfrReader; + +public class GCHeapSummary extends Event { + public final int gcId; + public final boolean afterGC; + public final long committed; + public final long reserved; + public final long used; + + public GCHeapSummary(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.afterGC = jfr.getVarint() > 0; + long start = jfr.getVarlong(); + long committedEnd = jfr.getVarlong(); + this.committed = jfr.getVarlong(); + long reservedEnd = jfr.getVarlong(); + this.reserved = jfr.getVarlong(); + this.used = jfr.getVarlong(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java new file mode 100644 index 00000000000..a7f7d60cb79 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java @@ -0,0 +1,43 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class LiveObject extends Event { + public final int classId; + public final long allocationSize; + public final long allocationTime; + + public LiveObject(long time, int tid, int stackTraceId, int classId, long allocationSize, long allocationTime) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.allocationTime = allocationTime; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof LiveObject) { + LiveObject a = (LiveObject) o; + return classId == a.classId; + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return allocationSize; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java new file mode 100644 index 00000000000..07249391540 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java @@ -0,0 +1,22 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +public class MallocEvent extends Event { + public final long address; + public final long size; + + public MallocEvent(long time, int tid, int stackTraceId, long address, long size) { + super(time, tid, stackTraceId); + this.address = address; + this.size = size; + } + + @Override + public long value() { + return size; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java new file mode 100644 index 00000000000..31c57467c33 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java @@ -0,0 +1,65 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +public class MallocLeakAggregator implements EventCollector { + private final EventCollector wrapped; + private final Map addresses; + private List events; + + public MallocLeakAggregator(EventCollector wrapped) { + this.wrapped = wrapped; + this.addresses = new HashMap<>(); + } + + @Override + public void collect(Event e) { + events.add((MallocEvent) e); + } + + @Override + public void beforeChunk() { + events = new ArrayList<>(); + } + + @Override + public void afterChunk() { + events.sort(null); + + for (MallocEvent e : events) { + if (e.size > 0) { + addresses.put(e.address, e); + } else { + addresses.remove(e.address); + } + } + + events = null; + } + + @Override + public boolean finish() { + wrapped.beforeChunk(); + for (Event e : addresses.values()) { + wrapped.collect(e); + } + wrapped.afterChunk(); + + // Free memory before the final conversion + addresses.clear(); + return true; + } + + @Override + public void forEach(Visitor visitor) { + wrapped.forEach(visitor); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java new file mode 100644 index 00000000000..fc0329558fd --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java @@ -0,0 +1,23 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.protocol.jfr.jfr.event; + +import io.sentry.protocol.jfr.jfr.JfrReader; + +public class ObjectCount extends Event { + public final int gcId; + public final int classId; + public final long count; + public final long totalSize; + + public ObjectCount(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.classId = jfr.getVarint(); + this.count = jfr.getVarlong(); + this.totalSize = jfr.getVarlong(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java new file mode 100644 index 00000000000..5e9dcea90f7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -0,0 +1,353 @@ +package io.sentry.protocol.profiling; + +import io.sentry.DataCategory; +import io.sentry.IContinuousProfiler; +import io.sentry.ILogger; +import io.sentry.IScopes; +import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; +import io.sentry.NoOpScopes; +import io.sentry.ProfileChunk; +import io.sentry.ProfileLifecycle; +import io.sentry.Sentry; +import io.sentry.SentryDate; +import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; +import io.sentry.SentryOptions; +import io.sentry.SentryUUID; +import io.sentry.TracesSampler; +import io.sentry.protocol.SentryId; +import io.sentry.transport.RateLimiter; +import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.SentryRandom; +import one.profiler.AsyncProfiler; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.sentry.DataCategory.All; +import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; + +@ApiStatus.Internal +public final class JavaContinuousProfiler + implements IContinuousProfiler, RateLimiter.IRateLimitObserver { + private static final long MAX_CHUNK_DURATION_MILLIS = 10000; + + private final @NotNull ILogger logger; + private final @Nullable String profilingTracesDirPath; + private final int profilingTracesHz; + private final @NotNull ISentryExecutorService executorService; + private boolean isInitialized = false; + private boolean isRunning = false; + private @Nullable IScopes scopes; + private @Nullable Future stopFuture; + private final @NotNull List payloadBuilders = new ArrayList<>(); + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + private @NotNull SentryId chunkId = SentryId.EMPTY_ID; + private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); + private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); + + private @NotNull String filename = ""; + + private final @NotNull AsyncProfiler profiler; + private volatile boolean shouldSample = true; + private boolean shouldStop = false; + private boolean isSampled = false; + private int rootSpanCounter = 0; + + private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock(); + + public JavaContinuousProfiler( + final @NotNull ILogger logger, + final @Nullable String profilingTracesDirPath, + final int profilingTracesHz, + final @NotNull ISentryExecutorService executorService) { + this.logger = logger; + this.profilingTracesDirPath = profilingTracesDirPath; + this.profilingTracesHz = profilingTracesHz; + this.executorService = executorService; + this.profiler = AsyncProfiler.getInstance(); + } + + private void init() { + // We initialize it only once + if (isInitialized) { + return; + } + isInitialized = true; + if (profilingTracesDirPath == null) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); + return; + } + if (profilingTracesHz <= 0) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + profilingTracesHz); + return; + } + +// profiler = +// new AndroidProfiler( +// profilingTracesDirPath, +// (int) SECONDS.toMicros(1) / profilingTracesHz, +// frameMetricsCollector, +// null, +// logger); + } + + @SuppressWarnings("ReferenceEquality") + @Override + public void startProfiler( + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (shouldSample) { + isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); + //Kepp TRUE for now +// shouldSample = false; + } + if (!isSampled) { + logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); + return; + } + + if (!isRunning()) { + logger.log(SentryLevel.DEBUG, "Started Profiler."); + start(); + } + } + } + + @SuppressWarnings("ReferenceEquality") + private void start() { + if ((scopes == null || scopes == NoOpScopes.getInstance()) + && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { + this.scopes = Sentry.forkedRootScopes("profiler"); + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null) { + rateLimiter.addRateLimitObserver(this); + } + } + + // Let's initialize trace folder and profiling interval + init(); + // init() didn't create profiler, should never happen + if (profiler == null) { + return; + } + + if (scopes != null) { + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null + && (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + // Let's stop and reset profiler id, as the profile is now broken anyway + stop(false); + return; + } + + // 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(); + } + filename = SentryUUID.generateSentryId() + ".jfr"; + final String startData; + try { + startData = profiler.execute("start,jfr,event=cpu,alloc,file=" + filename); + } catch (IOException e) { + throw new RuntimeException(e); + } + // check if profiling started + if (startData == null) { + return; + } + + isRunning = true; + + if (SentryId.EMPTY_ID.equals(profilerId)) { + profilerId = new SentryId(); + } + + if (chunkId == SentryId.EMPTY_ID) { + chunkId = new SentryId(); + } + + try { + stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + } catch (RejectedExecutionException e) { + logger.log( + SentryLevel.ERROR, + "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", + e); + shouldStop = true; + } + } + + @Override + public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + shouldStop = true; + } + } + + private void stop(final boolean restartProfiler) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (stopFuture != null) { + stopFuture.cancel(true); + } + // check if profiler was created and it's running + if (profiler == null || !isRunning) { + // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the + // ids + profilerId = SentryId.EMPTY_ID; + chunkId = SentryId.EMPTY_ID; + return; + } + + String endData = null; + try { + endData = profiler.execute("stop,jfr"); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // check if profiler end successfully + if (endData == null) { + logger.log( + SentryLevel.ERROR, + "An error occurred while collecting a profile chunk, and it won't be sent."); + } else { + // The scopes can be null if the profiler is started before the SDK is initialized (app + // 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()) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, + chunkId, + new HashMap<>(), + new File(filename), + startProfileChunkTimestamp)); + } + } + + isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; + filename = ""; + + if (scopes != null) { + sendChunks(scopes, scopes.getOptions()); + } + + if (restartProfiler && !shouldStop) { + logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); + start(); + } else { + // When the profiler is stopped manually, we have to reset its id + profilerId = SentryId.EMPTY_ID; + logger.log(SentryLevel.DEBUG, "Profile chunk finished."); + } + } + } + + @Override + public void reevaluateSampling() { + shouldSample = true; + } + + @Override + public void close(final boolean isTerminating) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + rootSpanCounter = 0; + shouldStop = true; + if (isTerminating) { + stop(false); + isClosed.set(true); + } + } + } + + @Override + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + try { + options + .getExecutorService() + .submit( + () -> { + // SDK is closed, we don't send the chunks + if (isClosed.get()) { + return; + } + final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); + try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) { + for (ProfileChunk.Builder builder : payloadBuilders) { + payloads.add(builder.build(options)); + } + payloadBuilders.clear(); + } + for (ProfileChunk payload : payloads) { + scopes.captureProfileChunk(payload); + } + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e); + } + } + + @Override + public boolean isRunning() { + return isRunning; + } + + @VisibleForTesting + @Nullable + Future getStopFuture() { + return stopFuture; + } + + @VisibleForTesting + public int getRootSpanCounter() { + return rootSpanCounter; + } + + @Override + public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { + // We stop the profiler as soon as we are rate limited, to avoid the performance overhead +// if (rateLimiter.isActiveForCategory(All) +// || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { +// logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); +// stop(false); +// } + // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's + // useless to restart it automatically + } +} + diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java new file mode 100644 index 00000000000..b5d42551de9 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java @@ -0,0 +1,70 @@ +package io.sentry.protocol.profiling; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Map; + +import io.sentry.ILogger; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectWriter; + +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"; + } + + @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/JfrProfile.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java new file mode 100644 index 00000000000..d504e5457b5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java @@ -0,0 +1,130 @@ +package io.sentry.protocol.profiling; +import io.sentry.profilemeasurements.ProfileMeasurement; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import io.sentry.ILogger; +import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectReader; +import io.sentry.ObjectWriter; +import io.sentry.ProfileChunk; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class JfrProfile implements JsonUnknown, JsonSerializable { + public @Nullable List samples; + + public @Nullable List> stacks; // List of frame indices + + public @Nullable List frames; + + public @Nullable Map threadMetadata; // 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(); + } + + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class JsonKeys { + public static final String SAMPLES = "samples"; + public static final String STACKS = "stacks"; + public static final String FRAMES = "frames"; + public static final String THREAD_METADATA = "thread_metadata"; + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull JfrProfile deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + JfrProfile data = new JfrProfile(); + 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; + } + } + +} diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java new file mode 100644 index 00000000000..1d86714e656 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java @@ -0,0 +1,63 @@ +package io.sentry.protocol.profiling; + +import io.sentry.JsonDeserializer; +import io.sentry.ObjectReader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import io.sentry.ILogger; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectWriter; + +public final class JfrSample implements JsonUnknown, JsonSerializable { + + public double timestamp; // Unix timestamp in seconds with microsecond precision + + public int stackId; + + public @Nullable String threadId; + + public static final class JsonKeys { + public static final String TIMESTAMP = "timestamp"; + public static final String STACK_ID = "stackId"; + public static final String THREAD_ID = "threadId"; + } + + @Override + public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { + writer.beginObject(); + writer.name(JfrSample.JsonKeys.TIMESTAMP).value(logger, timestamp); + writer.name(JfrSample.JsonKeys.STACK_ID).value(logger, stackId); + + if(threadId != null) { + writer.name(JfrFrame.JsonKeys.FILENAME).value(logger, threadId); + } + + writer.endObject(); + } + + @Override + public @Nullable Map getUnknown() { + return new HashMap<>(); + } + + @Override + public void setUnknown(@Nullable Map unknown) { + + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull JfrSample deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + JfrSample data = new JfrSample(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java new file mode 100644 index 00000000000..560059b763e --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java @@ -0,0 +1,347 @@ +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/ThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java new file mode 100644 index 00000000000..7072807d932 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java @@ -0,0 +1,57 @@ +package io.sentry.protocol.profiling; + +import io.sentry.ILogger; +import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; +import io.sentry.ObjectReader; +import io.sentry.ObjectWriter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public final class ThreadMetadata implements JsonUnknown, JsonSerializable { + public @Nullable String name; // e.g., "com.example.MyClass.myMethod" + + public int priority; // e.g., "com.example" (package name) + + public static final class JsonKeys { + public static final String NAME = "name"; + public static final String PRIORITY = "priority"; + } + + + @Override + public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { + writer.beginObject(); + if (name != null) { + writer.name(JsonKeys.NAME).value(logger, name); + } + writer.name(JsonKeys.PRIORITY).value(logger, priority); + writer.endObject(); + } + + @Override + public @Nullable Map getUnknown() { + return new HashMap<>(); + } + + @Override + public void setUnknown(@Nullable Map unknown) { + + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull ThreadMetadata deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + ThreadMetadata data = new ThreadMetadata(); + return data; + } + } +} + diff --git a/sentry/src/test/java/io/sentry/JavaProfilerTest.kt b/sentry/src/test/java/io/sentry/JavaProfilerTest.kt new file mode 100644 index 00000000000..bf4d24e45c7 --- /dev/null +++ b/sentry/src/test/java/io/sentry/JavaProfilerTest.kt @@ -0,0 +1,36 @@ +package io.sentry + +import io.sentry.protocol.ViewHierarchy +import one.profiler.AsyncProfiler +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class JavaProfilerTest { + + private class Fixture { + val contentType = "application/json" + val filename = "logs.txt" + val bytes = "content".toByteArray() + val pathname = "path/to/$filename" + } + + private val fixture = Fixture() + + @Test + fun `testprofilerone`() { + val profiler = AsyncProfiler.getInstance() + val startResult = profiler.execute("start,jfr,event=wall,alloc,loop=5s,file=test88-%t.jfr"); + println(startResult) + + for (i in 1..20) { + println(i) + Thread.sleep(100) + } + + var endResult = profiler.execute("stop,jfr,file=myNewFile.jfr"); + println(endResult) + } +} diff --git a/sentry/test88-20250408-152005.jfr b/sentry/test88-20250408-152005.jfr new file mode 100644 index 0000000000000000000000000000000000000000..76e629fcb7f6d0f84b26f155629b6d4ba4d18ede GIT binary patch literal 57839 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 literal 0 HcmV?d00001 diff --git a/sentry/test88-20250408-152146.jfr b/sentry/test88-20250408-152146.jfr new file mode 100644 index 0000000000000000000000000000000000000000..54296763239be7392ff1367f10366a86915c11bf GIT binary patch literal 85992 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 Date: Mon, 19 May 2025 17:31:44 +0200 Subject: [PATCH 02/31] re-add data category profile_chunk, fix json naming, new converter based on jfr converter bundled with asyncprofiler --- .../src/main/java/io/sentry/DataCategory.java | 1 + .../io/sentry/protocol/jfr/convert/Frame.java | 1 + .../io/sentry/protocol/jfr/convert/Index.java | 1 + ...AsyncProfilerToSentryProfileConverter.java | 144 ++++++++++++++++++ .../protocol/jfr/convert/JfrToHeatmap.java | 96 ------------ .../sentry/protocol/profiling/JfrFrame.java | 3 + .../sentry/protocol/profiling/JfrSample.java | 10 +- 7 files changed, 155 insertions(+), 101 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java delete mode 100644 sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index f6b13b62485..9978d9f15d2 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -15,6 +15,7 @@ public enum DataCategory { Monitor("monitor"), Profile("profile"), ProfileChunkUi("profile_chunk_ui"), + ProfileChunk("profile_chunk"), Transaction("transaction"), Replay("replay"), Span("span"), diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java index 8cac02b5ca6..74859e88aa3 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java @@ -8,6 +8,7 @@ import java.util.HashMap; public class Frame extends HashMap { + private static final long serialVersionUID = 1L; public static final byte TYPE_INTERPRETED = 0; public static final byte TYPE_JIT_COMPILED = 1; public static final byte TYPE_INLINED = 2; diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java index b0f93b242d4..e7240ee78f8 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java @@ -9,6 +9,7 @@ import java.util.HashMap; public class Index extends HashMap { + private static final long serialVersionUID = 1L; private final Class cls; public Index(Class cls, T empty) { diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java new file mode 100644 index 00000000000..60d9472d1ab --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -0,0 +1,144 @@ +package io.sentry.protocol.jfr.convert; + +import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.protocol.jfr.jfr.StackTrace; +import io.sentry.protocol.jfr.jfr.event.Event; +import io.sentry.protocol.profiling.JfrFrame; +import io.sentry.protocol.profiling.JfrProfile; +import io.sentry.protocol.profiling.JfrSample; +import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +import io.sentry.protocol.profiling.ThreadMetadata; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { + private JfrProfile jfrProfile = new JfrProfile(); + + public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { + super(jfr, args); + } + + public static void main(String[] args) throws IOException { + + Path jfrPath = Paths.get("/Users/lukasbloder/development/projects/sentry/sentry-java/197d8e97cb514418b15e5578026f39f2.jfr"); + JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); + JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); + System.out.println("Done"); + } + + @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(jfrProfile.threadMetadata == null) { + jfrProfile.threadMetadata = new HashMap<>(); + } + + jfrProfile.threadMetadata.computeIfAbsent(String.valueOf(event.tid), k -> { + ThreadMetadata metadata = new ThreadMetadata(); + metadata.name = getThreadName(event.tid); + metadata.priority = 0; + return metadata; + }); + } + + if(jfrProfile.samples == null) { + jfrProfile.samples = new ArrayList<>(); + } + + if(jfrProfile.frames == null) { + jfrProfile.frames = new ArrayList<>(); + } + + List stack = new ArrayList<>(); + int currentStack = stacks.size(); + int currentFrame = jfrProfile.frames.size(); + for (int i = 0; i < methods.length; i++) { +// for (int i = methods.length; --i >= 0; ) { + JfrFrame frame = new JfrFrame(); + StackTraceElement element = getStackTraceElement(methods[i], types[i], locations[i]); + final String classNameWithLambdas = element.getClassName().replace("/", "."); + frame.function = classNameWithLambdas + "." + element.getMethodName(); + + int lastDot = classNameWithLambdas.lastIndexOf('.'); + int firstDollar = classNameWithLambdas.indexOf('$'); + if (lastDot > 0 && lastDot > firstDollar) { + frame.module = classNameWithLambdas.substring(0, lastDot); + } else if (firstDollar > 0) { + frame.module = classNameWithLambdas.substring(0, firstDollar); + } else if (!classNameWithLambdas.startsWith("[")) { + frame.module = ""; + } + frame.lineno = (element.getLineNumber() != 0) ? element.getLineNumber() : null; + frame.filename = classNameWithLambdas; + + jfrProfile.frames.add(frame); + stack.add(currentFrame); + currentFrame++; + + System.out.println(element.getMethodName()); + System.out.println(element.getClassName()); + System.out.println(element.getLineNumber()); + System.out.println(element.getFileName()); + } + + + 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(event.tid); + sample.stackId = currentStack; + jfrProfile.samples.add(sample); + + stacks.add(stack); + } + } + }); + jfrProfile.stacks = stacks; + System.out.println("Samples: " + events.size()); + } + + public static JfrProfile convertFromFile(Path jfrFilePath) throws IOException { + JfrAsyncProfilerToSentryProfileConverter converter; + try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { + Arguments args = new Arguments(); + args.cpu = false; + args.alloc = false; + args.threads = true; + args.lines = true; + args.dot = true; + + converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args); + converter.convert(); + } + + return converter.jfrProfile; + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java deleted file mode 100644 index d26ba5ae736..00000000000 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToHeatmap.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import one.heatmap.Heatmap; -import one.jfr.Dictionary; -import one.jfr.JfrReader; -import one.jfr.StackTrace; -import one.jfr.event.AllocationSample; -import one.jfr.event.ContendedLock; -import one.jfr.event.Event; -import one.jfr.event.EventCollector; - -import java.io.*; - -import static one.convert.Frame.TYPE_INLINED; -import static one.convert.Frame.TYPE_KERNEL; - -public class JfrToHeatmap extends JfrConverter { - private final Heatmap heatmap; - - public JfrToHeatmap(JfrReader jfr, Arguments args) { - super(jfr, args); - this.heatmap = new Heatmap(args, this); - } - - @Override - protected EventCollector createCollector(Arguments args) { - return new EventCollector() { - @Override - public void collect(Event event) { - int extra = 0; - byte type = 0; - if (event instanceof AllocationSample) { - extra = ((AllocationSample) event).classId; - type = ((AllocationSample) event).tlabSize == 0 ? TYPE_KERNEL : TYPE_INLINED; - } else if (event instanceof ContendedLock) { - extra = ((ContendedLock) event).classId; - type = TYPE_KERNEL; - } - - long msFromStart = (event.time - jfr.chunkStartTicks) * 1_000 / jfr.ticksPerSec; - long timeMs = jfr.chunkStartNanos / 1_000_000 + msFromStart; - - heatmap.addEvent(event.stackTraceId, extra, type, timeMs); - } - - @Override - public void beforeChunk() { - heatmap.beforeChunk(); - jfr.stackTraces.forEach(new Dictionary.Visitor() { - @Override - public void visit(long key, StackTrace trace) { - heatmap.addStack(key, trace.methods, trace.locations, trace.types, trace.methods.length); - } - }); - } - - @Override - public void afterChunk() { - jfr.stackTraces.clear(); - } - - @Override - public boolean finish() { - heatmap.finish(jfr.startNanos / 1_000_000); - return false; - } - - @Override - public void forEach(Visitor visitor) { - throw new AssertionError("Should not be called"); - } - }; - } - - public void dump(OutputStream out) throws IOException { - try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { - heatmap.dump(ps); - } - } - - public static void convert(String input, String output, Arguments args) throws IOException { - JfrToHeatmap converter; - try (JfrReader jfr = new JfrReader(input)) { - converter = new JfrToHeatmap(jfr, args); - converter.convert(); - } - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(output))) { - converter.dump(out); - } - } -} diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java index b5d42551de9..f23e42848fe 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java @@ -32,6 +32,7 @@ public static final class JsonKeys { 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 @@ -41,9 +42,11 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr 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); } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java index 1d86714e656..65b89418ec0 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java @@ -24,18 +24,18 @@ public final class JfrSample implements JsonUnknown, JsonSerializable { public static final class JsonKeys { public static final String TIMESTAMP = "timestamp"; - public static final String STACK_ID = "stackId"; - public static final String THREAD_ID = "threadId"; + public static final String STACK_ID = "stack_id"; + public static final String THREAD_ID = "thread_id"; } @Override public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); - writer.name(JfrSample.JsonKeys.TIMESTAMP).value(logger, timestamp); - writer.name(JfrSample.JsonKeys.STACK_ID).value(logger, stackId); + writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); + writer.name(JsonKeys.STACK_ID).value(logger, stackId); if(threadId != null) { - writer.name(JfrFrame.JsonKeys.FILENAME).value(logger, threadId); + writer.name(JsonKeys.THREAD_ID).value(logger, threadId); } writer.endObject(); From 558ff79f74b69d37aec30556fc2e2d3f30e0f1b1 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 16 Jun 2025 11:46:28 +0200 Subject: [PATCH 03/31] read java thread ids from jfr and use those instead of os thread ids, use existing SentryStackFrame instead of JfrFrame, --- .../java/io/sentry/SentryEnvelopeItem.java | 8 +- ...AsyncProfilerToSentryProfileConverter.java | 56 +- .../io/sentry/protocol/jfr/jfr/JfrReader.java | 2 + .../profiling/JavaContinuousProfiler.java | 1 + .../sentry/protocol/profiling/JfrProfile.java | 3 +- .../JfrToSentryProfileConverter.java | 692 +++++++++--------- 6 files changed, 391 insertions(+), 371 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index dce5621c0b7..2cb0fe2c2ce 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -7,8 +7,9 @@ import io.sentry.clientreport.ClientReport; import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SentryTransaction; +import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; import io.sentry.protocol.profiling.JfrProfile; -import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +//import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; @@ -296,7 +297,8 @@ private static void ensureAttachmentSizeLimit( traceFile.getName())); } if(traceFile.getName().endsWith(".jfr")) { - JfrProfile profile = new JfrToSentryProfileConverter().convert(traceFile.toPath()); +// JfrProfile profile = new JfrToSentryProfileConverter().convert(traceFile.toPath()); + JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(traceFile.toPath()); profileChunk.setJfrProfile(profile); } else { @@ -321,7 +323,7 @@ private static void ensureAttachmentSizeLimit( String.format("Failed to serialize profile chunk\n%s", e.getMessage())); } finally { // In any case we delete the trace file - traceFile.delete(); +// traceFile.delete(); } }); diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java index 60d9472d1ab..8c011c052ff 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -1,12 +1,15 @@ package io.sentry.protocol.jfr.convert; +import io.sentry.Sentry; +import io.sentry.SentryStackTraceFactory; +import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.StackTrace; import io.sentry.protocol.jfr.jfr.event.Event; import io.sentry.protocol.profiling.JfrFrame; import io.sentry.protocol.profiling.JfrProfile; import io.sentry.protocol.profiling.JfrSample; -import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +//import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.protocol.profiling.ThreadMetadata; import java.io.IOException; @@ -16,9 +19,10 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Objects; public class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { - private JfrProfile jfrProfile = new JfrProfile(); + private final JfrProfile jfrProfile = new JfrProfile(); public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { super(jfr, args); @@ -26,9 +30,9 @@ public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { public static void main(String[] args) throws IOException { - Path jfrPath = Paths.get("/Users/lukasbloder/development/projects/sentry/sentry-java/197d8e97cb514418b15e5578026f39f2.jfr"); + Path jfrPath = Paths.get("/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); - JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); +// JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); System.out.println("Done"); } @@ -56,7 +60,10 @@ public void visit(Event event, long value) { jfrProfile.threadMetadata = new HashMap<>(); } - jfrProfile.threadMetadata.computeIfAbsent(String.valueOf(event.tid), k -> { + + long threadIdToUse = jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; + + jfrProfile.threadMetadata.computeIfAbsent(String.valueOf(threadIdToUse), k -> { ThreadMetadata metadata = new ThreadMetadata(); metadata.name = getThreadName(event.tid); metadata.priority = 0; @@ -77,31 +84,37 @@ public void visit(Event event, long value) { int currentFrame = jfrProfile.frames.size(); for (int i = 0; i < methods.length; i++) { // for (int i = methods.length; --i >= 0; ) { - JfrFrame frame = new JfrFrame(); + SentryStackFrame frame = new SentryStackFrame(); StackTraceElement element = getStackTraceElement(methods[i], types[i], locations[i]); final String classNameWithLambdas = element.getClassName().replace("/", "."); - frame.function = classNameWithLambdas + "." + element.getMethodName(); + frame.setFunction(element.getMethodName()); - int lastDot = classNameWithLambdas.lastIndexOf('.'); int firstDollar = classNameWithLambdas.indexOf('$'); - if (lastDot > 0 && lastDot > firstDollar) { - frame.module = classNameWithLambdas.substring(0, lastDot); - } else if (firstDollar > 0) { - frame.module = classNameWithLambdas.substring(0, firstDollar); + 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.module = ""; + frame.setModule(""); } - frame.lineno = (element.getLineNumber() != 0) ? element.getLineNumber() : null; - frame.filename = classNameWithLambdas; + + 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); jfrProfile.frames.add(frame); stack.add(currentFrame); currentFrame++; - - System.out.println(element.getMethodName()); - System.out.println(element.getClassName()); - System.out.println(element.getLineNumber()); - System.out.println(element.getFileName()); } @@ -113,7 +126,8 @@ public void visit(Event event, long value) { double timestampDouble = instant.getEpochSecond() + instant.getNano() / 1_000_000_000.0; sample.timestamp = timestampDouble; - sample.threadId = String.valueOf(event.tid); +// sample.threadId = String.valueOf(event.tid); + sample.threadId = String.valueOf(jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid); sample.stackId = currentStack; jfrProfile.samples.add(sample); diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java index 5aad97a002d..ecae4b1b4db 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java @@ -53,6 +53,7 @@ public class JfrReader implements Closeable { public final Dictionary types = new Dictionary<>(); public final Map typesByName = new HashMap<>(); public final Dictionary threads = new Dictionary<>(); + public final Dictionary javaThreads = new Dictionary<>(); public final Dictionary classes = new Dictionary<>(); public final Dictionary strings = new Dictionary<>(); public final Dictionary symbols = new Dictionary<>(); @@ -430,6 +431,7 @@ private void readThreads(int fieldCount) { String javaName = getString(); long javaThreadId = getVarlong(); readFields(fieldCount - 4); + javaThreads.put(id, javaThreadId); threads.put(id, javaName != null ? javaName : osName); } } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java index 5e9dcea90f7..1a34a7de461 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -175,6 +175,7 @@ private void start() { filename = SentryUUID.generateSentryId() + ".jfr"; final String startData; try { +// System.out.println("### Starting profiler with start,jfr,event=wall,file"); startData = profiler.execute("start,jfr,event=cpu,alloc,file=" + filename); } catch (IOException e) { throw new RuntimeException(e); diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java index d504e5457b5..271034de024 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java @@ -3,6 +3,7 @@ import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; +import io.sentry.protocol.SentryStackFrame; import io.sentry.vendor.gson.stream.JsonToken; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +27,7 @@ public final class JfrProfile implements JsonUnknown, JsonSerializable { public @Nullable List> stacks; // List of frame indices - public @Nullable List frames; + public @Nullable List frames; public @Nullable Map threadMetadata; // Key is Thread ID (String) diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java index 560059b763e..e2946f2939b 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java @@ -1,347 +1,347 @@ -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; +//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(); // } - 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); - } - } -} +//// 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); +// } +// } +//} From 04c82de16b4d7bb4c6d95efa2a702cb3b0cb66f5 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 23 Jun 2025 09:16:58 +0200 Subject: [PATCH 04/31] adhere to sentry conventions re format null safety etc, fix compilation --- sentry/api/sentry.api | 439 ++++++ .../src/main/java/io/sentry/ProfileChunk.java | 16 +- .../java/io/sentry/SentryEnvelopeItem.java | 21 +- .../main/java/io/sentry/SentryOptions.java | 8 +- .../protocol/jfr/convert/Arguments.java | 210 +-- .../protocol/jfr/convert/CallStack.java | 36 +- .../protocol/jfr/convert/Classifier.java | 246 +-- .../protocol/jfr/convert/FlameGraph.java | 673 +++++---- .../io/sentry/protocol/jfr/convert/Frame.java | 88 +- .../io/sentry/protocol/jfr/convert/Index.java | 70 +- ...AsyncProfilerToSentryProfileConverter.java | 219 +-- .../protocol/jfr/convert/JfrConverter.java | 446 +++--- .../protocol/jfr/convert/JfrToFlame.java | 129 +- .../jfr/convert/ResourceProcessor.java | 41 +- .../io/sentry/protocol/jfr/jfr/ClassRef.java | 10 +- .../sentry/protocol/jfr/jfr/Dictionary.java | 182 ++- .../protocol/jfr/jfr/DictionaryInt.java | 198 ++- .../io/sentry/protocol/jfr/jfr/Element.java | 9 +- .../io/sentry/protocol/jfr/jfr/JfrClass.java | 48 +- .../io/sentry/protocol/jfr/jfr/JfrField.java | 20 +- .../io/sentry/protocol/jfr/jfr/JfrReader.java | 1337 +++++++++-------- .../io/sentry/protocol/jfr/jfr/MethodRef.java | 18 +- .../sentry/protocol/jfr/jfr/StackTrace.java | 18 +- .../jfr/jfr/event/AllocationSample.java | 59 +- .../protocol/jfr/jfr/event/CPULoad.java | 20 +- .../protocol/jfr/jfr/event/ContendedLock.java | 54 +- .../sentry/protocol/jfr/jfr/event/Event.java | 90 +- .../jfr/jfr/event/EventAggregator.java | 256 ++-- .../jfr/jfr/event/EventCollector.java | 18 +- .../jfr/jfr/event/ExecutionSample.java | 32 +- .../protocol/jfr/jfr/event/GCHeapSummary.java | 34 +- .../protocol/jfr/jfr/event/LiveObject.java | 59 +- .../protocol/jfr/jfr/event/MallocEvent.java | 24 +- .../jfr/jfr/event/MallocLeakAggregator.java | 90 +- .../protocol/jfr/jfr/event/ObjectCount.java | 24 +- .../profiling/JavaContinuousProfiler.java | 136 +- .../sentry/protocol/profiling/JfrFrame.java | 32 +- .../sentry/protocol/profiling/JfrProfile.java | 105 +- .../sentry/protocol/profiling/JfrSample.java | 23 +- .../JfrToSentryProfileConverter.java | 109 +- .../protocol/profiling/ThreadMetadata.java | 14 +- 41 files changed, 3088 insertions(+), 2573 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6dec9900a0a..467337416cf 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -356,6 +356,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field LogItem Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; + public static final field ProfileChunk Lio/sentry/DataCategory; public static final field ProfileChunkUi Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; @@ -1938,11 +1939,13 @@ 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;Lio/sentry/SentryOptions;)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 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 getJfrProfile ()Lio/sentry/protocol/profiling/JfrProfile; public fun getMeasurements ()Ljava/util/Map; public fun getPlatform ()Ljava/lang/String; public fun getProfilerId ()Lio/sentry/protocol/SentryId; @@ -1955,6 +1958,7 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr public fun hashCode ()I public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setJfrProfile (Lio/sentry/protocol/profiling/JfrProfile;)V public fun setSampledProfile (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V } @@ -1975,6 +1979,7 @@ public final class io/sentry/ProfileChunk$JsonKeys { public static final field CLIENT_SDK Ljava/lang/String; public static final field DEBUG_META Ljava/lang/String; public static final field ENVIRONMENT Ljava/lang/String; + public static final field JRF_PROFILE Ljava/lang/String; public static final field MEASUREMENTS Ljava/lang/String; public static final field PLATFORM Ljava/lang/String; public static final field PROFILER_ID Ljava/lang/String; @@ -6087,6 +6092,440 @@ public final class io/sentry/protocol/ViewHierarchyNode$JsonKeys { public fun ()V } +public final class io/sentry/protocol/jfr/convert/Arguments { + public field alloc Z + public field bci Z + public field classify Z + public field cpu Z + public field dot Z + public field exclude Ljava/util/regex/Pattern; + public final field files Ljava/util/List; + public field from J + public field grain D + public field help Z + public field highlight Ljava/lang/String; + public field include Ljava/util/regex/Pattern; + public field inverted Z + public field leak Z + public field lines Z + public field live Z + public field lock Z + public field minwidth D + public field nativemem Z + public field norm Z + public field output Ljava/lang/String; + public field reverse Z + public field simple Z + public field skip I + public field state Ljava/lang/String; + public field threads Z + public field title Ljava/lang/String; + public field to J + public field total Z + public field wall Z + public fun ([Ljava/lang/String;)V +} + +public final class io/sentry/protocol/jfr/convert/CallStack { + public fun ()V + public fun clear ()V + public fun pop ()V + public fun push (Ljava/lang/String;B)V +} + +public final class io/sentry/protocol/jfr/convert/FlameGraph : java/util/Comparator { + public fun (Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun addSample (Lio/sentry/protocol/jfr/convert/CallStack;J)V + public fun compare (Lio/sentry/protocol/jfr/convert/Frame;Lio/sentry/protocol/jfr/convert/Frame;)I + public synthetic fun compare (Ljava/lang/Object;Ljava/lang/Object;)I + public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun dump (Ljava/io/PrintStream;)V + public fun parseCollapsed (Ljava/io/Reader;)V + public fun parseHtml (Ljava/io/Reader;)V +} + +public final class io/sentry/protocol/jfr/convert/Frame : java/util/HashMap { + public static final field TYPE_C1_COMPILED B + public static final field TYPE_CPP B + public static final field TYPE_INLINED B + public static final field TYPE_INTERPRETED B + public static final field TYPE_JIT_COMPILED B + public static final field TYPE_KERNEL B + public static final field TYPE_NATIVE B +} + +public final class io/sentry/protocol/jfr/convert/Index : java/util/HashMap { + public fun (Ljava/lang/Class;Ljava/lang/Object;)V + public fun (Ljava/lang/Class;Ljava/lang/Object;I)V + public fun index (Ljava/lang/Object;)I + public fun keys ()[Ljava/lang/Object; + public fun keys ([Ljava/lang/Object;)V +} + +public final class io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/protocol/jfr/convert/JfrConverter { + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + public static fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/JfrProfile; + public static fun main ([Ljava/lang/String;)V +} + +public abstract class io/sentry/protocol/jfr/convert/JfrConverter { + protected final field args Lio/sentry/protocol/jfr/convert/Arguments; + protected final field collector Lio/sentry/protocol/jfr/jfr/event/EventCollector; + protected final field jfr Lio/sentry/protocol/jfr/jfr/JfrReader; + protected field methodNames Lio/sentry/protocol/jfr/jfr/Dictionary; + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + protected fun collectEvents ()V + public fun convert ()V + protected fun convertChunk ()V + protected fun createCollector (Lio/sentry/protocol/jfr/convert/Arguments;)Lio/sentry/protocol/jfr/jfr/event/EventCollector; + public synthetic fun getCategory (Lio/sentry/protocol/jfr/jfr/StackTrace;)Lio/sentry/protocol/jfr/convert/Classifier$Category; + public fun getClassName (J)Ljava/lang/String; + public fun getMethodName (JB)Ljava/lang/String; + public fun getStackTraceElement (JBI)Ljava/lang/StackTraceElement; + public fun getThreadName (I)Ljava/lang/String; + protected fun getThreadStates (Z)Ljava/util/BitSet; + protected fun isNativeFrame (B)Z + protected fun toThreadState (Ljava/lang/String;)I + protected fun toTicks (J)J +} + +protected abstract class io/sentry/protocol/jfr/convert/JfrConverter$AggregatedEventVisitor : io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { + protected fun (Lio/sentry/protocol/jfr/convert/JfrConverter;)V + protected abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;J)V + public final fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V +} + +public final class io/sentry/protocol/jfr/convert/JfrToFlame : io/sentry/protocol/jfr/convert/JfrConverter { + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun dump (Ljava/io/OutputStream;)V +} + +public final class io/sentry/protocol/jfr/convert/ResourceProcessor { + public fun ()V + public static fun getResource (Ljava/lang/String;)Ljava/lang/String; + public static fun printTill (Ljava/io/PrintStream;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +} + +public final class io/sentry/protocol/jfr/jfr/ClassRef { + public final field name J + public fun (J)V +} + +public final class io/sentry/protocol/jfr/jfr/Dictionary { + public fun ()V + public fun (I)V + public fun clear ()V + public fun forEach (Lio/sentry/protocol/jfr/jfr/Dictionary$Visitor;)V + public fun get (J)Ljava/lang/Object; + public fun preallocate (I)I + public fun put (JLjava/lang/Object;)V + public fun size ()I +} + +public abstract interface class io/sentry/protocol/jfr/jfr/Dictionary$Visitor { + public abstract fun visit (JLjava/lang/Object;)V +} + +public final class io/sentry/protocol/jfr/jfr/DictionaryInt { + public fun ()V + public fun (I)V + public fun clear ()V + public fun forEach (Lio/sentry/protocol/jfr/jfr/DictionaryInt$Visitor;)V + public fun get (J)I + public fun get (JI)I + public fun preallocate (I)I + public fun put (JI)V +} + +public abstract interface class io/sentry/protocol/jfr/jfr/DictionaryInt$Visitor { + public abstract fun visit (JI)V +} + +public final class io/sentry/protocol/jfr/jfr/JfrClass { + public fun field (Ljava/lang/String;)Lio/sentry/protocol/jfr/jfr/JfrField; +} + +public final class io/sentry/protocol/jfr/jfr/JfrField { +} + +public final class io/sentry/protocol/jfr/jfr/JfrReader : java/io/Closeable { + public field chunkEndNanos J + public field chunkStartNanos J + public field chunkStartTicks J + public final field classes Lio/sentry/protocol/jfr/jfr/Dictionary; + public field endNanos J + public final field enums Ljava/util/Map; + public final field javaThreads Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field methods Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field settings Ljava/util/Map; + public final field stackTraces Lio/sentry/protocol/jfr/jfr/Dictionary; + public field startNanos J + public field startTicks J + public field stopAtNewChunk Z + public final field strings Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field symbols Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field threads Lio/sentry/protocol/jfr/jfr/Dictionary; + public field ticksPerSec J + public final field types Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field typesByName Ljava/util/Map; + public fun (Ljava/lang/String;)V + public fun (Ljava/nio/ByteBuffer;)V + public fun close ()V + public fun durationNanos ()J + public fun eof ()Z + public fun getBytes ()[B + public fun getDouble ()D + public fun getEnumKey (Ljava/lang/String;Ljava/lang/String;)I + public fun getEnumValue (Ljava/lang/String;I)Ljava/lang/String; + public fun getFloat ()F + public fun getString ()Ljava/lang/String; + public fun getVarint ()I + public fun getVarlong ()J + public fun hasMoreChunks ()Z + public fun incomplete ()Z + public fun readAllEvents ()Ljava/util/List; + public fun readAllEvents (Ljava/lang/Class;)Ljava/util/List; + public fun readEvent ()Lio/sentry/protocol/jfr/jfr/event/Event; + public fun readEvent (Ljava/lang/Class;)Lio/sentry/protocol/jfr/jfr/event/Event; + public fun registerEvent (Ljava/lang/String;Ljava/lang/Class;)V +} + +public final class io/sentry/protocol/jfr/jfr/MethodRef { + public final field cls J + public final field name J + public final field sig J + public fun (JJJ)V +} + +public final class io/sentry/protocol/jfr/jfr/StackTrace { + public final field locations [I + public final field methods [J + public final field types [B + public fun ([J[B[I)V +} + +public final class io/sentry/protocol/jfr/jfr/event/AllocationSample : io/sentry/protocol/jfr/jfr/event/Event { + public final field allocationSize J + public final field classId I + public final field tlabSize J + public fun (JIIIJJ)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/CPULoad : io/sentry/protocol/jfr/jfr/event/Event { + public final field jvmSystem F + public final field jvmUser F + public final field machineTotal F + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ContendedLock : io/sentry/protocol/jfr/jfr/event/Event { + public final field classId I + public final field duration J + public fun (JIIJI)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public abstract class io/sentry/protocol/jfr/jfr/event/Event : java/lang/Comparable { + public final field stackTraceId I + public final field tid I + public final field time J + protected fun (JII)V + public fun classId ()J + public fun compareTo (Lio/sentry/protocol/jfr/jfr/event/Event;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun samples ()J + public fun toString ()Ljava/lang/String; + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/EventAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { + public fun (ZD)V + public fun afterChunk ()V + public fun beforeChunk ()V + public fun coarsen (D)V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V + public fun finish ()Z + public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V + public fun size ()I +} + +public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector { + public abstract fun afterChunk ()V + public abstract fun beforeChunk ()V + public abstract fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public abstract fun finish ()Z + public abstract fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V +} + +public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { + public abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ExecutionSample : io/sentry/protocol/jfr/jfr/event/Event { + public final field samples I + public final field threadState I + public fun (JIIII)V + public fun samples ()J + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/GCHeapSummary : io/sentry/protocol/jfr/jfr/event/Event { + public final field afterGC Z + public final field committed J + public final field gcId I + public final field reserved J + public final field used J + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/LiveObject : io/sentry/protocol/jfr/jfr/event/Event { + public final field allocationSize J + public final field allocationTime J + public final field classId I + public fun (JIIIJJ)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/MallocEvent : io/sentry/protocol/jfr/jfr/event/Event { + public final field address J + public final field size J + public fun (JIIJJ)V + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { + public fun (Lio/sentry/protocol/jfr/jfr/event/EventCollector;)V + public fun afterChunk ()V + public fun beforeChunk ()V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public fun finish ()Z + public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ObjectCount : io/sentry/protocol/jfr/jfr/event/Event { + public final field classId I + public final field count J + public final field gcId I + public final field totalSize J + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { + public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V + public fun close (Z)V + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRootSpanCounter ()I + public fun isRunning ()Z + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V + public fun reevaluateSampling ()V + public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfiler (Lio/sentry/ProfileLifecycle;)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/JfrProfile : 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 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/JfrProfile$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/JfrProfile; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/profiling/JfrProfile$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/JfrSample : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public field stackId I + public field threadId Ljava/lang/String; + public field timestamp D + 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/JfrSample$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/JfrSample; + 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 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 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/ThreadMetadata$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/ThreadMetadata; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/profiling/ThreadMetadata$JsonKeys { + public static final field NAME Ljava/lang/String; + public static final field PRIORITY Ljava/lang/String; + public fun ()V +} + public final class io/sentry/rrweb/RRWebBreadcrumbEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field EVENT_TAG Ljava/lang/String; public fun ()V diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index e95c214c501..d9a88cc1e62 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -63,19 +63,19 @@ public ProfileChunk( this.clientSdk = options.getSdkVersion(); this.release = options.getRelease() != null ? options.getRelease() : ""; this.environment = options.getEnvironment(); - this.platform = "java"; + this.platform = "android"; this.version = "2"; this.timestamp = timestamp; } public ProfileChunk( - final @NotNull SentryId profilerId, - final @NotNull SentryId chunkId, - final @NotNull File traceFile, - final @NotNull Map measurements, - final @NotNull Double timestamp, - final @NotNull String platform, - final @NotNull SentryOptions options) { + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull Double timestamp, + final @NotNull String platform, + final @NotNull SentryOptions options) { this.profilerId = profilerId; this.chunkId = chunkId; this.traceFile = traceFile; diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 2cb0fe2c2ce..87acd2176d3 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -9,7 +9,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; import io.sentry.protocol.profiling.JfrProfile; -//import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +// import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; @@ -19,7 +19,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -296,18 +295,20 @@ private static void ensureAttachmentSizeLimit( "Dropping profile chunk, because the file '%s' doesn't exists", traceFile.getName())); } - if(traceFile.getName().endsWith(".jfr")) { -// JfrProfile profile = new JfrToSentryProfileConverter().convert(traceFile.toPath()); - JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(traceFile.toPath()); + if (traceFile.getName().endsWith(".jfr")) { + // JfrProfile profile = new + // JfrToSentryProfileConverter().convert(traceFile.toPath()); + JfrProfile profile = + JfrAsyncProfilerToSentryProfileConverter.convertFromFile(traceFile.toPath()); profileChunk.setJfrProfile(profile); } else { // The payload of the profile item is a json including the trace file encoded with // base64 final byte[] traceFileBytes = - readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); + readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); final @NotNull String base64Trace = - Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); if (base64Trace.isEmpty()) { throw new SentryEnvelopeException("Profiling trace file is empty"); } @@ -315,15 +316,15 @@ private static void ensureAttachmentSizeLimit( } try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { serializer.serialize(profileChunk, writer); return stream.toByteArray(); } catch (IOException e) { throw new SentryEnvelopeException( - String.format("Failed to serialize profile chunk\n%s", e.getMessage())); + String.format("Failed to serialize profile chunk\n%s", e.getMessage())); } finally { // In any case we delete the trace file -// traceFile.delete(); + traceFile.delete(); } }); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index b476d483dfd..547811fc5e3 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -16,7 +16,6 @@ import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryTransaction; -import io.sentry.protocol.profiling.JavaContinuousProfiler; import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; @@ -555,7 +554,7 @@ public class SentryOptions { * means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null * (disabled). */ - private @Nullable Double profileSessionSampleRate = 1.0; + private @Nullable Double profileSessionSampleRate; /** * Whether the profiling lifecycle is controlled manually or based on the trace lifecycle. @@ -3049,7 +3048,10 @@ private SentryOptions(final boolean empty) { setSentryClientName(BuildConfig.SENTRY_JAVA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(sdkVersion); addPackageInfo(); - setContinuousProfiler(new JavaContinuousProfiler(new SystemOutLogger(), "", 10, executorService)); + // TODO: make this configurable + // setProfileSessionSampleRate(1.0); + // setContinuousProfiler( + // new JavaContinuousProfiler(new SystemOutLogger(), "", 10, executorService)); } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java index 7b48136ea24..8d34033ee6e 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java @@ -9,120 +9,122 @@ import java.lang.reflect.Modifier; import java.util.*; import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public class Arguments { - public String title = "Flame Graph"; - public String highlight; - public String output; - public String state; - public Pattern include; - public Pattern exclude; - public double minwidth; - public double grain; - public int skip; - public boolean help; - public boolean reverse; - public boolean inverted; - public boolean cpu; - public boolean wall; - public boolean alloc; - public boolean nativemem; - public boolean leak; - public boolean live; - public boolean lock; - public boolean threads; - public boolean classify; - public boolean total; - public boolean lines; - public boolean bci; - public boolean simple; - public boolean norm; - public boolean dot; - public long from; - public long to; - public final List files = new ArrayList<>(); +public final class Arguments { + public @NotNull String title = "Flame Graph"; + public @Nullable String highlight; + public @Nullable String output; + public @Nullable String state; + public @Nullable Pattern include; + public @Nullable Pattern exclude; + public double minwidth; + public double grain; + public int skip; + public boolean help; + public boolean reverse; + public boolean inverted; + public boolean cpu; + public boolean wall; + public boolean alloc; + public boolean nativemem; + public boolean leak; + public boolean live; + public boolean lock; + public boolean threads; + public boolean classify; + public boolean total; + public boolean lines; + public boolean bci; + public boolean simple; + public boolean norm; + public boolean dot; + public long from; + public long to; + public final List files = new ArrayList<>(); - public Arguments(String... args) { - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - String fieldName; - if (arg.startsWith("--")) { - fieldName = arg.substring(2); - } else if (arg.startsWith("-") && arg.length() == 2) { - fieldName = alias(arg.charAt(1)); - } else { - files.add(arg); - continue; - } + public Arguments(String... args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + String fieldName; + if (arg.startsWith("--")) { + fieldName = arg.substring(2); + } else if (arg.startsWith("-") && arg.length() == 2) { + fieldName = alias(arg.charAt(1)); + } else { + files.add(arg); + continue; + } - try { - Field f = Arguments.class.getDeclaredField(fieldName); - if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) { - throw new IllegalArgumentException(arg); - } - - Class type = f.getType(); - if (type == String.class) { - f.set(this, args[++i]); - } else if (type == boolean.class) { - f.setBoolean(this, true); - } else if (type == int.class) { - f.setInt(this, Integer.parseInt(args[++i])); - } else if (type == double.class) { - f.setDouble(this, Double.parseDouble(args[++i])); - } else if (type == long.class) { - f.setLong(this, parseTimestamp(args[++i])); - } else if (type == Pattern.class) { - f.set(this, Pattern.compile(args[++i])); - } - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new IllegalArgumentException(arg); - } + try { + Field f = Arguments.class.getDeclaredField(fieldName); + if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) { + throw new IllegalArgumentException(arg); } - } - private static String alias(char c) { - switch (c) { - case 'h': - return "help"; - case 'o': - return "output"; - case 'r': - return "reverse"; - case 'i': - return "inverted"; - case 'I': - return "include"; - case 'X': - return "exclude"; - case 't': - return "threads"; - case 's': - return "state"; - default: - return String.valueOf(c); + Class type = f.getType(); + if (type == String.class) { + f.set(this, args[++i]); + } else if (type == boolean.class) { + f.setBoolean(this, true); + } else if (type == int.class) { + f.setInt(this, Integer.parseInt(args[++i])); + } else if (type == double.class) { + f.setDouble(this, Double.parseDouble(args[++i])); + } else if (type == long.class) { + f.setLong(this, parseTimestamp(args[++i])); + } else if (type == Pattern.class) { + f.set(this, Pattern.compile(args[++i])); } + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalArgumentException(arg); + } } + } - // Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S - private static long parseTimestamp(String time) { - if (time.indexOf(':') < 0) { - return Long.parseLong(time); - } + private static String alias(char c) { + switch (c) { + case 'h': + return "help"; + case 'o': + return "output"; + case 'r': + return "reverse"; + case 'i': + return "inverted"; + case 'I': + return "include"; + case 'X': + return "exclude"; + case 't': + return "threads"; + case 's': + return "state"; + default: + return String.valueOf(c); + } + } - GregorianCalendar cal = new GregorianCalendar(); - StringTokenizer st = new StringTokenizer(time, "-:.T"); + // Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S + private static long parseTimestamp(String time) { + if (time.indexOf(':') < 0) { + return Long.parseLong(time); + } - if (time.indexOf('T') > 0) { - cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken())); - cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1); - cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken())); - } - cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + GregorianCalendar cal = new GregorianCalendar(); + StringTokenizer st = new StringTokenizer(time, "-:.T"); - return cal.getTimeInMillis(); + if (time.indexOf('T') > 0) { + cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken())); + cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1); + cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken())); } + cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + + return cal.getTimeInMillis(); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java index a75807b5a7a..dbd62a192c2 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java @@ -7,26 +7,26 @@ import java.util.Arrays; -public class CallStack { - String[] names = new String[16]; - byte[] types = new byte[16]; - int size; +public final class CallStack { + String[] names = new String[16]; + byte[] types = new byte[16]; + int size; - public void push(String name, byte type) { - if (size >= names.length) { - names = Arrays.copyOf(names, size * 2); - types = Arrays.copyOf(types, size * 2); - } - names[size] = name; - types[size] = type; - size++; + public void push(String name, byte type) { + if (size >= names.length) { + names = Arrays.copyOf(names, size * 2); + types = Arrays.copyOf(types, size * 2); } + names[size] = name; + types[size] = type; + size++; + } - public void pop() { - size--; - } + public void pop() { + size--; + } - public void clear() { - size = 0; - } + public void clear() { + size = 0; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java index 71f106c0c4b..ba33655c58e 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java @@ -5,142 +5,150 @@ package io.sentry.protocol.jfr.convert; -import io.sentry.protocol.jfr.jfr.StackTrace; - import static io.sentry.protocol.jfr.convert.Frame.*; +import io.sentry.protocol.jfr.jfr.StackTrace; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + abstract class Classifier { - enum Category { - GC("[gc]", TYPE_CPP), - JIT("[jit]", TYPE_CPP), - VM("[vm]", TYPE_CPP), - VTABLE_STUBS("[vtable_stubs]", TYPE_NATIVE), - NATIVE("[native]", TYPE_NATIVE), - INTERPRETER("[Interpreter]", TYPE_NATIVE), - C1_COMP("[c1_comp]", TYPE_C1_COMPILED), - C2_COMP("[c2_comp]", TYPE_INLINED), - ADAPTER("[c2i_adapter]", TYPE_INLINED), - CLASS_INIT("[class_init]", TYPE_CPP), - CLASS_LOAD("[class_load]", TYPE_CPP), - CLASS_RESOLVE("[class_resolve]", TYPE_CPP), - CLASS_VERIFY("[class_verify]", TYPE_CPP), - LAMBDA_INIT("[lambda_init]", TYPE_CPP); + enum Category { + GC("[gc]", TYPE_CPP), + JIT("[jit]", TYPE_CPP), + VM("[vm]", TYPE_CPP), + VTABLE_STUBS("[vtable_stubs]", TYPE_NATIVE), + NATIVE("[native]", TYPE_NATIVE), + INTERPRETER("[Interpreter]", TYPE_NATIVE), + C1_COMP("[c1_comp]", TYPE_C1_COMPILED), + C2_COMP("[c2_comp]", TYPE_INLINED), + ADAPTER("[c2i_adapter]", TYPE_INLINED), + CLASS_INIT("[class_init]", TYPE_CPP), + CLASS_LOAD("[class_load]", TYPE_CPP), + CLASS_RESOLVE("[class_resolve]", TYPE_CPP), + CLASS_VERIFY("[class_verify]", TYPE_CPP), + LAMBDA_INIT("[lambda_init]", TYPE_CPP); - final String title; - final byte type; + final String title; + final byte type; - Category(String title, byte type) { - this.title = title; - this.type = type; - } + Category(String title, byte type) { + this.title = title; + this.type = type; } + } - public Category getCategory(StackTrace stackTrace) { - long[] methods = stackTrace.methods; - byte[] types = stackTrace.types; + public @Nullable Category getCategory(@NotNull StackTrace stackTrace) { + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; - Category category; - if ((category = detectGcJit(methods, types)) == null && - (category = detectClassLoading(methods, types)) == null) { - category = detectOther(methods, types); - } - return category; + Category category; + if ((category = detectGcJit(methods, types)) == null + && (category = detectClassLoading(methods, types)) == null) { + category = detectOther(methods, types); } + return category; + } - private Category detectGcJit(long[] methods, byte[] types) { - boolean vmThread = false; - for (int i = types.length; --i >= 0; ) { - if (types[i] == TYPE_CPP) { - switch (getMethodName(methods[i], types[i])) { - case "CompileBroker::compiler_thread_loop": - return Category.JIT; - case "GCTaskThread::run": - case "WorkerThread::run": - return Category.GC; - case "java_start": - case "thread_native_entry": - vmThread = true; - break; - } - } else if (types[i] != TYPE_NATIVE) { - break; - } + private @Nullable Category detectGcJit(long[] methods, byte[] types) { + boolean vmThread = false; + for (int i = types.length; --i >= 0; ) { + if (types[i] == TYPE_CPP) { + switch (getMethodName(methods[i], types[i])) { + case "CompileBroker::compiler_thread_loop": + return Category.JIT; + case "GCTaskThread::run": + case "WorkerThread::run": + return Category.GC; + case "java_start": + case "thread_native_entry": + vmThread = true; + break; } - return vmThread ? Category.VM : null; + } else if (types[i] != TYPE_NATIVE) { + break; + } } + return vmThread ? Category.VM : null; + } - private Category detectClassLoading(long[] methods, byte[] types) { - for (int i = 0; i < methods.length; i++) { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.equals("Verifier::verify")) { - return Category.CLASS_VERIFY; - } else if (methodName.startsWith("InstanceKlass::initialize")) { - return Category.CLASS_INIT; - } else if (methodName.startsWith("LinkResolver::") || - methodName.startsWith("InterpreterRuntime::resolve") || - methodName.startsWith("SystemDictionary::resolve")) { - return Category.CLASS_RESOLVE; - } else if (methodName.endsWith("ClassLoader.loadClass")) { - return Category.CLASS_LOAD; - } else if (methodName.endsWith("LambdaMetafactory.metafactory") || - methodName.endsWith("LambdaMetafactory.altMetafactory")) { - return Category.LAMBDA_INIT; - } else if (methodName.endsWith("table stub")) { - return Category.VTABLE_STUBS; - } else if (methodName.equals("Interpreter")) { - return Category.INTERPRETER; - } else if (methodName.startsWith("I2C/C2I")) { - return i + 1 < types.length && types[i + 1] == TYPE_INTERPRETED ? Category.INTERPRETER : Category.ADAPTER; - } - } - return null; + private @Nullable Category detectClassLoading(long[] methods, byte[] types) { + for (int i = 0; i < methods.length; i++) { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.equals("Verifier::verify")) { + return Category.CLASS_VERIFY; + } else if (methodName.startsWith("InstanceKlass::initialize")) { + return Category.CLASS_INIT; + } else if (methodName.startsWith("LinkResolver::") + || methodName.startsWith("InterpreterRuntime::resolve") + || methodName.startsWith("SystemDictionary::resolve")) { + return Category.CLASS_RESOLVE; + } else if (methodName.endsWith("ClassLoader.loadClass")) { + return Category.CLASS_LOAD; + } else if (methodName.endsWith("LambdaMetafactory.metafactory") + || methodName.endsWith("LambdaMetafactory.altMetafactory")) { + return Category.LAMBDA_INIT; + } else if (methodName.endsWith("table stub")) { + return Category.VTABLE_STUBS; + } else if (methodName.equals("Interpreter")) { + return Category.INTERPRETER; + } else if (methodName.startsWith("I2C/C2I")) { + return i + 1 < types.length && types[i + 1] == TYPE_INTERPRETED + ? Category.INTERPRETER + : Category.ADAPTER; + } } + return null; + } - private Category detectOther(long[] methods, byte[] types) { - boolean inJava = true; - for (int i = 0; i < types.length; i++) { - switch (types[i]) { - case TYPE_INTERPRETED: - return inJava ? Category.INTERPRETER : Category.NATIVE; - case TYPE_JIT_COMPILED: - return inJava ? Category.C2_COMP : Category.NATIVE; - case TYPE_INLINED: - inJava = true; - break; - case TYPE_NATIVE: { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.startsWith("JVM_") || methodName.startsWith("Unsafe_") || - methodName.startsWith("MHN_") || methodName.startsWith("jni_")) { - return Category.VM; - } - switch (methodName) { - case "call_stub": - case "deoptimization": - case "unknown_Java": - case "not_walkable_Java": - case "InlineCacheBuffer": - return Category.VM; - } - if (methodName.endsWith("_arraycopy") || methodName.contains("pthread_cond")) { - break; - } - inJava = false; - break; - } - case TYPE_CPP: { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.startsWith("Runtime1::")) { - return Category.C1_COMP; - } - break; - } - case TYPE_C1_COMPILED: - return inJava ? Category.C1_COMP : Category.NATIVE; + private @NotNull Category detectOther(long[] methods, byte[] types) { + boolean inJava = true; + for (int i = 0; i < types.length; i++) { + switch (types[i]) { + case TYPE_INTERPRETED: + return inJava ? Category.INTERPRETER : Category.NATIVE; + case TYPE_JIT_COMPILED: + return inJava ? Category.C2_COMP : Category.NATIVE; + case TYPE_INLINED: + inJava = true; + break; + case TYPE_NATIVE: + { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("JVM_") + || methodName.startsWith("Unsafe_") + || methodName.startsWith("MHN_") + || methodName.startsWith("jni_")) { + return Category.VM; } - } - return Category.NATIVE; + switch (methodName) { + case "call_stub": + case "deoptimization": + case "unknown_Java": + case "not_walkable_Java": + case "InlineCacheBuffer": + return Category.VM; + } + if (methodName.endsWith("_arraycopy") || methodName.contains("pthread_cond")) { + break; + } + inJava = false; + break; + } + case TYPE_CPP: + { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("Runtime1::")) { + return Category.C1_COMP; + } + break; + } + case TYPE_C1_COMPILED: + return inJava ? Category.C1_COMP : Category.NATIVE; + } } + return Category.NATIVE; + } - protected abstract String getMethodName(long method, byte type); + protected abstract @NotNull String getMethodName(long method, byte type); } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java index 1d662019f99..022ce746d96 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java @@ -5,391 +5,404 @@ package io.sentry.protocol.jfr.convert; +import static io.sentry.protocol.jfr.convert.Frame.*; +import static io.sentry.protocol.jfr.convert.ResourceProcessor.*; + import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Comparator; import java.util.StringTokenizer; import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; + +public final class FlameGraph implements Comparator { + private static final Frame[] EMPTY_FRAME_ARRAY = {}; + private static final String[] FRAME_SUFFIX = {"_[0]", "_[j]", "_[i]", "", "", "_[k]", "_[1]"}; + private static final byte HAS_SUFFIX = (byte) 0x80; + private static final int FLUSH_THRESHOLD = 15000; + + private final Arguments args; + private final Index cpool = new Index<>(String.class, ""); + private final Frame root = new Frame(0, TYPE_NATIVE); + private final StringBuilder outbuf = new StringBuilder(FLUSH_THRESHOLD + 1000); + private @NotNull int[] order; + private int depth; + private int lastLevel; + private long lastX; + private long lastTotal; + private long mintotal; + + public FlameGraph(@NotNull Arguments args) { + this.args = args; + this.order = new int[0]; // Initialize with empty array + } + + public void parseCollapsed(Reader in) throws IOException { + CallStack stack = new CallStack(); + + try (BufferedReader br = new BufferedReader(in)) { + for (String line; (line = br.readLine()) != null; ) { + int space = line.lastIndexOf(' '); + if (space <= 0) continue; + + long ticks = Long.parseLong(line.substring(space + 1)); + + for (int from = 0, to; from < space; from = to + 1) { + if ((to = line.indexOf(';', from)) < 0) to = space; + String name = line.substring(from, to); + byte type = detectType(name); + if ((type & HAS_SUFFIX) != 0) { + name = name.substring(0, name.length() - 4); + type ^= HAS_SUFFIX; + } + stack.push(name, type); + } -import static io.sentry.protocol.jfr.convert.Frame.*; -import static io.sentry.protocol.jfr.convert.ResourceProcessor.*; - -public class FlameGraph implements Comparator { - private static final Frame[] EMPTY_FRAME_ARRAY = {}; - private static final String[] FRAME_SUFFIX = {"_[0]", "_[j]", "_[i]", "", "", "_[k]", "_[1]"}; - private static final byte HAS_SUFFIX = (byte) 0x80; - private static final int FLUSH_THRESHOLD = 15000; - - private final Arguments args; - private final Index cpool = new Index<>(String.class, ""); - private final Frame root = new Frame(0, TYPE_NATIVE); - private final StringBuilder outbuf = new StringBuilder(FLUSH_THRESHOLD + 1000); - private int[] order; - private int depth; - private int lastLevel; - private long lastX; - private long lastTotal; - private long mintotal; - - public FlameGraph(Arguments args) { - this.args = args; + addSample(stack, ticks); + stack.clear(); + } } - - public void parseCollapsed(Reader in) throws IOException { - CallStack stack = new CallStack(); - - try (BufferedReader br = new BufferedReader(in)) { - for (String line; (line = br.readLine()) != null; ) { - int space = line.lastIndexOf(' '); - if (space <= 0) continue; - - long ticks = Long.parseLong(line.substring(space + 1)); - - for (int from = 0, to; from < space; from = to + 1) { - if ((to = line.indexOf(';', from)) < 0) to = space; - String name = line.substring(from, to); - byte type = detectType(name); - if ((type & HAS_SUFFIX) != 0) { - name = name.substring(0, name.length() - 4); - type ^= HAS_SUFFIX; - } - stack.push(name, type); - } - - addSample(stack, ticks); - stack.clear(); - } + } + + public void parseHtml(Reader in) throws IOException { + Frame[] levels = new Frame[128]; + int level = 0; + long total = 0; + boolean needRebuild = args.reverse || args.include != null || args.exclude != null; + + try (BufferedReader br = new BufferedReader(in)) { + while (!br.readLine().startsWith("const cpool")) + ; + br.readLine(); + + String s = ""; + for (String line; (line = br.readLine()).startsWith("'"); ) { + String packed = unescape(line.substring(1, line.lastIndexOf('\''))); + s = s.substring(0, packed.charAt(0) - ' ').concat(packed.substring(1)); + cpool.put(s, cpool.size()); + } + + while (!br.readLine().isEmpty()) + ; + + for (String line; !(line = br.readLine()).isEmpty(); ) { + StringTokenizer st = new StringTokenizer(line.substring(2, line.length() - 1), ","); + int nameAndType = Integer.parseInt(st.nextToken()); + + char func = line.charAt(0); + if (func == 'f') { + level = Integer.parseInt(st.nextToken()); + st.nextToken(); + } else if (func == 'u') { + level++; + } else if (func != 'n') { + throw new IllegalStateException("Unexpected line: " + line); } - } - public void parseHtml(Reader in) throws IOException { - Frame[] levels = new Frame[128]; - int level = 0; - long total = 0; - boolean needRebuild = args.reverse || args.include != null || args.exclude != null; - - try (BufferedReader br = new BufferedReader(in)) { - while (!br.readLine().startsWith("const cpool")) ; - br.readLine(); - - String s = ""; - for (String line; (line = br.readLine()).startsWith("'"); ) { - String packed = unescape(line.substring(1, line.lastIndexOf('\''))); - s = s.substring(0, packed.charAt(0) - ' ').concat(packed.substring(1)); - cpool.put(s, cpool.size()); - } - - while (!br.readLine().isEmpty()) ; - - for (String line; !(line = br.readLine()).isEmpty(); ) { - StringTokenizer st = new StringTokenizer(line.substring(2, line.length() - 1), ","); - int nameAndType = Integer.parseInt(st.nextToken()); - - char func = line.charAt(0); - if (func == 'f') { - level = Integer.parseInt(st.nextToken()); - st.nextToken(); - } else if (func == 'u') { - level++; - } else if (func != 'n') { - throw new IllegalStateException("Unexpected line: " + line); - } - - if (st.hasMoreTokens()) { - total = Long.parseLong(st.nextToken()); - } - - int titleIndex = nameAndType >>> 3; - byte type = (byte) (nameAndType & 7); - if (st.hasMoreTokens() && (type <= TYPE_INLINED || type >= TYPE_C1_COMPILED)) { - type = TYPE_JIT_COMPILED; - } - - Frame f = level > 0 || needRebuild ? new Frame(titleIndex, type) : root; - f.self = f.total = total; - if (st.hasMoreTokens()) f.inlined = Long.parseLong(st.nextToken()); - if (st.hasMoreTokens()) f.c1 = Long.parseLong(st.nextToken()); - if (st.hasMoreTokens()) f.interpreted = Long.parseLong(st.nextToken()); - - if (level > 0) { - Frame parent = levels[level - 1]; - parent.put(f.key, f); - parent.self -= total; - depth = Math.max(depth, level); - } - if (level >= levels.length) { - levels = Arrays.copyOf(levels, level * 2); - } - levels[level] = f; - } + if (st.hasMoreTokens()) { + total = Long.parseLong(st.nextToken()); } - if (needRebuild) { - rebuild(levels[0], new CallStack(), cpool.keys()); + int titleIndex = nameAndType >>> 3; + byte type = (byte) (nameAndType & 7); + if (st.hasMoreTokens() && (type <= TYPE_INLINED || type >= TYPE_C1_COMPILED)) { + type = TYPE_JIT_COMPILED; } - } - private void rebuild(Frame frame, CallStack stack, String[] strings) { - if (frame.self > 0) { - addSample(stack, frame.self); + Frame f = level > 0 || needRebuild ? new Frame(titleIndex, type) : root; + f.self = f.total = total; + if (st.hasMoreTokens()) f.inlined = Long.parseLong(st.nextToken()); + if (st.hasMoreTokens()) f.c1 = Long.parseLong(st.nextToken()); + if (st.hasMoreTokens()) f.interpreted = Long.parseLong(st.nextToken()); + + if (level > 0) { + Frame parent = levels[level - 1]; + parent.put(f.key, f); + parent.self -= total; + depth = Math.max(depth, level); } - if (!frame.isEmpty()) { - for (Frame child : frame.values()) { - stack.push(strings[child.getTitleIndex()], child.getType()); - rebuild(child, stack, strings); - stack.pop(); - } + if (level >= levels.length) { + levels = Arrays.copyOf(levels, level * 2); } + levels[level] = f; + } } - public void addSample(CallStack stack, long ticks) { - if (excludeStack(stack)) { - return; - } - - Frame frame = root; - if (args.reverse) { - for (int i = stack.size; --i >= args.skip; ) { - frame = addChild(frame, stack.names[i], stack.types[i], ticks); - } - } else { - for (int i = args.skip; i < stack.size; i++) { - frame = addChild(frame, stack.names[i], stack.types[i], ticks); - } - } - frame.total += ticks; - frame.self += ticks; - - depth = Math.max(depth, stack.size); + if (needRebuild) { + rebuild(levels[0], new CallStack(), cpool.keys()); } + } - public void dump(PrintStream out) { - mintotal = (long) (root.total * args.minwidth / 100); - - if ("collapsed".equals(args.output)) { - printFrameCollapsed(out, root, cpool.keys()); - return; - } - - String tail = getResource("/flame.html"); - - tail = printTill(out, tail, "/*height:*/300"); - int depth = mintotal > 1 ? root.depth(mintotal) : this.depth + 1; - out.print(Math.min(depth * 16, 32767)); + private void rebuild(Frame frame, CallStack stack, String[] strings) { + if (frame.self > 0) { + addSample(stack, frame.self); + } + if (!frame.isEmpty()) { + for (Frame child : frame.values()) { + stack.push(strings[child.getTitleIndex()], child.getType()); + rebuild(child, stack, strings); + stack.pop(); + } + } + } - tail = printTill(out, tail, "/*title:*/"); - out.print(args.title); + public void addSample(CallStack stack, long ticks) { + if (excludeStack(stack)) { + return; + } - // inverted toggles the layout for reversed stacktraces from icicle to flamegraph - // and for default stacktraces from flamegraphs to icicle. - tail = printTill(out, tail, "/*inverted:*/false"); - out.print(args.reverse ^ args.inverted); + Frame frame = root; + if (args.reverse) { + for (int i = stack.size; --i >= args.skip; ) { + frame = addChild(frame, stack.names[i], stack.types[i], ticks); + } + } else { + for (int i = args.skip; i < stack.size; i++) { + frame = addChild(frame, stack.names[i], stack.types[i], ticks); + } + } + frame.total += ticks; + frame.self += ticks; - tail = printTill(out, tail, "/*depth:*/0"); - out.print(depth); + depth = Math.max(depth, stack.size); + } - tail = printTill(out, tail, "/*cpool:*/"); - printCpool(out); + public void dump(PrintStream out) { + mintotal = (long) (root.total * args.minwidth / 100); - tail = printTill(out, tail, "/*frames:*/"); - printFrame(out, root, 0, 0); - out.print(outbuf); + if ("collapsed".equals(args.output)) { + printFrameCollapsed(out, root, cpool.keys()); + return; + } - tail = printTill(out, tail, "/*highlight:*/"); - out.print(args.highlight != null ? "'" + escape(args.highlight) + "'" : ""); + String tail = getResource("/flame.html"); - out.print(tail); - } + tail = printTill(out, tail, "/*height:*/300"); + int depth = mintotal > 1 ? root.depth(mintotal) : this.depth + 1; + out.print(Math.min(depth * 16, 32767)); - private void printCpool(PrintStream out) { - String[] strings = cpool.keys(); - Arrays.sort(strings); - out.print("'all'"); - - order = new int[strings.length]; - String s = ""; - for (int i = 1; i < strings.length; i++) { - int prefixLen = Math.min(getCommonPrefix(s, s = strings[i]), 95); - out.print(",\n'" + escape((char) (prefixLen + ' ') + s.substring(prefixLen)) + "'"); - order[cpool.get(s)] = i; - } + tail = printTill(out, tail, "/*title:*/"); + out.print(args.title); - // cpool is not used beyond this point - cpool.clear(); - } + // inverted toggles the layout for reversed stacktraces from icicle to flamegraph + // and for default stacktraces from flamegraphs to icicle. + tail = printTill(out, tail, "/*inverted:*/false"); + out.print(args.reverse ^ args.inverted); - private void printFrame(PrintStream out, Frame frame, int level, long x) { - int nameAndType = order[frame.getTitleIndex()] << 3 | frame.getType(); - boolean hasExtraTypes = (frame.inlined | frame.c1 | frame.interpreted) != 0 && - frame.inlined < frame.total && frame.interpreted < frame.total; + tail = printTill(out, tail, "/*depth:*/0"); + out.print(depth); - char func = 'f'; - if (level == lastLevel + 1 && x == lastX) { - func = 'u'; - } else if (level == lastLevel && x == lastX + lastTotal) { - func = 'n'; - } + tail = printTill(out, tail, "/*cpool:*/"); + printCpool(out); - StringBuilder sb = outbuf.append(func).append('(').append(nameAndType); - if (func == 'f') { - sb.append(',').append(level).append(',').append(x - lastX); - } - if (frame.total != lastTotal || hasExtraTypes) { - sb.append(',').append(frame.total); - if (hasExtraTypes) { - sb.append(',').append(frame.inlined).append(',').append(frame.c1).append(',').append(frame.interpreted); - } - } - sb.append(")\n"); + tail = printTill(out, tail, "/*frames:*/"); + printFrame(out, root, 0, 0); + out.print(outbuf); - if (sb.length() > FLUSH_THRESHOLD) { - out.print(sb); - sb.setLength(0); - } + tail = printTill(out, tail, "/*highlight:*/"); + out.print(args.highlight != null ? "'" + escape(args.highlight) + "'" : ""); - lastLevel = level; - lastX = x; - lastTotal = frame.total; + out.print(tail); + } - Frame[] children = frame.values().toArray(EMPTY_FRAME_ARRAY); - Arrays.sort(children, this); + private void printCpool(PrintStream out) { + String[] strings = cpool.keys(); + Arrays.sort(strings); + out.print("'all'"); - x += frame.self; - for (Frame child : children) { - if (child.total >= mintotal) { - printFrame(out, child, level + 1, x); - } - x += child.total; - } + order = new int[strings.length]; + String s = ""; + for (int i = 1; i < strings.length; i++) { + int prefixLen = Math.min(getCommonPrefix(s, s = strings[i]), 95); + out.print(",\n'" + escape((char) (prefixLen + ' ') + s.substring(prefixLen)) + "'"); + order[cpool.index(s)] = i; } - private void printFrameCollapsed(PrintStream out, Frame frame, String[] strings) { - StringBuilder sb = outbuf; - int prevLength = sb.length(); - - if (frame != root) { - sb.append(strings[frame.getTitleIndex()]).append(FRAME_SUFFIX[frame.getType()]); - if (frame.self > 0) { - int tmpLength = sb.length(); - out.print(sb.append(' ').append(frame.self).append('\n')); - sb.setLength(tmpLength); - } - sb.append(';'); - } + // cpool is not used beyond this point + cpool.clear(); + } + + private void printFrame(PrintStream out, Frame frame, int level, long x) { + int nameAndType = order[frame.getTitleIndex()] << 3 | frame.getType(); + boolean hasExtraTypes = + (frame.inlined | frame.c1 | frame.interpreted) != 0 + && frame.inlined < frame.total + && frame.interpreted < frame.total; + + char func = 'f'; + if (level == lastLevel + 1 && x == lastX) { + func = 'u'; + } else if (level == lastLevel && x == lastX + lastTotal) { + func = 'n'; + } - if (!frame.isEmpty()) { - for (Frame child : frame.values()) { - if (child.total >= mintotal) { - printFrameCollapsed(out, child, strings); - } - } - } + StringBuilder sb = outbuf.append(func).append('(').append(nameAndType); + if (func == 'f') { + sb.append(',').append(level).append(',').append(x - lastX); + } + if (frame.total != lastTotal || hasExtraTypes) { + sb.append(',').append(frame.total); + if (hasExtraTypes) { + sb.append(',') + .append(frame.inlined) + .append(',') + .append(frame.c1) + .append(',') + .append(frame.interpreted); + } + } + sb.append(")\n"); - sb.setLength(prevLength); + if (sb.length() > FLUSH_THRESHOLD) { + out.print(sb); + sb.setLength(0); } - private boolean excludeStack(CallStack stack) { - Pattern include = args.include; - Pattern exclude = args.exclude; - if (include == null && exclude == null) { - return false; - } + lastLevel = level; + lastX = x; + lastTotal = frame.total; - for (int i = 0; i < stack.size; i++) { - if (exclude != null && exclude.matcher(stack.names[i]).matches()) { - return true; - } - if (include != null && include.matcher(stack.names[i]).matches()) { - if (exclude == null) return false; - include = null; - } - } + Frame[] children = frame.values().toArray(EMPTY_FRAME_ARRAY); + Arrays.sort(children, this); - return include != null; + x += frame.self; + for (Frame child : children) { + if (child.total >= mintotal) { + printFrame(out, child, level + 1, x); + } + x += child.total; } - - private Frame addChild(Frame frame, String title, byte type, long ticks) { - frame.total += ticks; - - int titleIndex = cpool.index(title); - - Frame child; - switch (type) { - case TYPE_INTERPRETED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).interpreted += ticks; - break; - case TYPE_INLINED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).inlined += ticks; - break; - case TYPE_C1_COMPILED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).c1 += ticks; - break; - default: - child = frame.getChild(titleIndex, type); - } - return child; + } + + private void printFrameCollapsed(PrintStream out, Frame frame, String[] strings) { + StringBuilder sb = outbuf; + int prevLength = sb.length(); + + if (!root.equals(frame)) { + sb.append(strings[frame.getTitleIndex()]).append(FRAME_SUFFIX[frame.getType()]); + if (frame.self > 0) { + int tmpLength = sb.length(); + out.print(sb.append(' ').append(frame.self).append('\n')); + sb.setLength(tmpLength); + } + sb.append(';'); } - private static byte detectType(String title) { - if (title.endsWith("_[j]")) { - return TYPE_JIT_COMPILED | HAS_SUFFIX; - } else if (title.endsWith("_[i]")) { - return TYPE_INLINED | HAS_SUFFIX; - } else if (title.endsWith("_[k]")) { - return TYPE_KERNEL | HAS_SUFFIX; - } else if (title.endsWith("_[0]")) { - return TYPE_INTERPRETED | HAS_SUFFIX; - } else if (title.endsWith("_[1]")) { - return TYPE_C1_COMPILED | HAS_SUFFIX; - } else if (title.contains("::") || title.startsWith("-[") || title.startsWith("+[")) { - return TYPE_CPP; - } else if (title.indexOf('/') > 0 && title.charAt(0) != '[' - || title.indexOf('.') > 0 && Character.isUpperCase(title.charAt(0))) { - return TYPE_JIT_COMPILED; - } else { - return TYPE_NATIVE; + if (!frame.isEmpty()) { + for (Frame child : frame.values()) { + if (child.total >= mintotal) { + printFrameCollapsed(out, child, strings); } + } } - private static int getCommonPrefix(String a, String b) { - int length = Math.min(a.length(), b.length()); - for (int i = 0; i < length; i++) { - if (a.charAt(i) != b.charAt(i) || a.charAt(i) > 127) { - return i; - } - } - return length; - } + sb.setLength(prevLength); + } - private static String escape(String s) { - if (s.indexOf('\\') >= 0) s = s.replace("\\", "\\\\"); - if (s.indexOf('\'') >= 0) s = s.replace("'", "\\'"); - return s; + private boolean excludeStack(CallStack stack) { + Pattern include = args.include; + Pattern exclude = args.exclude; + if (include == null && exclude == null) { + return false; } - private static String unescape(String s) { - if (s.indexOf('\'') >= 0) s = s.replace("\\'", "'"); - if (s.indexOf('\\') >= 0) s = s.replace("\\\\", "\\"); - return s; + for (int i = 0; i < stack.size; i++) { + if (exclude != null && exclude.matcher(stack.names[i]).matches()) { + return true; + } + if (include != null && include.matcher(stack.names[i]).matches()) { + if (exclude == null) return false; + include = null; + } } - @Override - public int compare(Frame f1, Frame f2) { - return order[f1.getTitleIndex()] - order[f2.getTitleIndex()]; + return include != null; + } + + private Frame addChild(Frame frame, String title, byte type, long ticks) { + frame.total += ticks; + + int titleIndex = cpool.index(title); + + Frame child; + switch (type) { + case TYPE_INTERPRETED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).interpreted += ticks; + break; + case TYPE_INLINED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).inlined += ticks; + break; + case TYPE_C1_COMPILED: + (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).c1 += ticks; + break; + default: + child = frame.getChild(titleIndex, type); } - - public static void convert(String input, String output, Arguments args) throws IOException { - FlameGraph fg = new FlameGraph(args); - try (InputStreamReader in = new InputStreamReader(new FileInputStream(input), StandardCharsets.UTF_8)) { - if (input.endsWith(".html")) { - fg.parseHtml(in); - } else { - fg.parseCollapsed(in); - } - } - try (PrintStream out = new PrintStream(output, "UTF-8")) { - fg.dump(out); - } + return child; + } + + @SuppressWarnings("OperatorPrecedence") + private static byte detectType(String title) { + if (title.endsWith("_[j]")) { + return TYPE_JIT_COMPILED | HAS_SUFFIX; + } else if (title.endsWith("_[i]")) { + return TYPE_INLINED | HAS_SUFFIX; + } else if (title.endsWith("_[k]")) { + return TYPE_KERNEL | HAS_SUFFIX; + } else if (title.endsWith("_[0]")) { + return TYPE_INTERPRETED | HAS_SUFFIX; + } else if (title.endsWith("_[1]")) { + return TYPE_C1_COMPILED | HAS_SUFFIX; + } else if (title.contains("::") || title.startsWith("-[") || title.startsWith("+[")) { + return TYPE_CPP; + } else if (title.indexOf('/') > 0 && title.charAt(0) != '[' + || title.indexOf('.') > 0 && Character.isUpperCase(title.charAt(0))) { + return TYPE_JIT_COMPILED; + } else { + return TYPE_NATIVE; + } + } + + private static int getCommonPrefix(String a, String b) { + int length = Math.min(a.length(), b.length()); + for (int i = 0; i < length; i++) { + if (a.charAt(i) != b.charAt(i) || a.charAt(i) > 127) { + return i; + } + } + return length; + } + + private static String escape(String s) { + if (s.indexOf('\\') >= 0) s = s.replace("\\", "\\\\"); + if (s.indexOf('\'') >= 0) s = s.replace("'", "\\'"); + return s; + } + + private static String unescape(String s) { + if (s.indexOf('\'') >= 0) s = s.replace("\\'", "'"); + if (s.indexOf('\\') >= 0) s = s.replace("\\\\", "\\"); + return s; + } + + @Override + public int compare(Frame f1, Frame f2) { + return order[f1.getTitleIndex()] - order[f2.getTitleIndex()]; + } + + public static void convert(String input, String output, Arguments args) throws IOException { + FlameGraph fg = new FlameGraph(args); + try (InputStreamReader in = + new InputStreamReader(new FileInputStream(input), StandardCharsets.UTF_8)) { + if (input.endsWith(".html")) { + fg.parseHtml(in); + } else { + fg.parseCollapsed(in); + } + } + try (PrintStream out = new PrintStream(output, "UTF-8")) { + fg.dump(out); } + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java index 74859e88aa3..c5c64e6341b 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java @@ -7,60 +7,60 @@ import java.util.HashMap; -public class Frame extends HashMap { +public final class Frame extends HashMap { private static final long serialVersionUID = 1L; - public static final byte TYPE_INTERPRETED = 0; - public static final byte TYPE_JIT_COMPILED = 1; - public static final byte TYPE_INLINED = 2; - public static final byte TYPE_NATIVE = 3; - public static final byte TYPE_CPP = 4; - public static final byte TYPE_KERNEL = 5; - public static final byte TYPE_C1_COMPILED = 6; + public static final byte TYPE_INTERPRETED = 0; + public static final byte TYPE_JIT_COMPILED = 1; + public static final byte TYPE_INLINED = 2; + public static final byte TYPE_NATIVE = 3; + public static final byte TYPE_CPP = 4; + public static final byte TYPE_KERNEL = 5; + public static final byte TYPE_C1_COMPILED = 6; - private static final int TYPE_SHIFT = 28; + private static final int TYPE_SHIFT = 28; - final int key; - long total; - long self; - long inlined, c1, interpreted; + final int key; + long total; + long self; + long inlined, c1, interpreted; - private Frame(int key) { - this.key = key; - } + private Frame(int key) { + this.key = key; + } - Frame(int titleIndex, byte type) { - this(titleIndex | type << TYPE_SHIFT); - } + Frame(int titleIndex, byte type) { + this(titleIndex | type << TYPE_SHIFT); + } - Frame getChild(int titleIndex, byte type) { - return super.computeIfAbsent(titleIndex | type << TYPE_SHIFT, Frame::new); - } + Frame getChild(int titleIndex, byte type) { + return super.computeIfAbsent(titleIndex | type << TYPE_SHIFT, Frame::new); + } - int getTitleIndex() { - return key & ((1 << TYPE_SHIFT) - 1); - } + int getTitleIndex() { + return key & ((1 << TYPE_SHIFT) - 1); + } - byte getType() { - if (inlined * 3 >= total) { - return TYPE_INLINED; - } else if (c1 * 2 >= total) { - return TYPE_C1_COMPILED; - } else if (interpreted * 2 >= total) { - return TYPE_INTERPRETED; - } else { - return (byte) (key >>> TYPE_SHIFT); - } + byte getType() { + if (inlined * 3 >= total) { + return TYPE_INLINED; + } else if (c1 * 2 >= total) { + return TYPE_C1_COMPILED; + } else if (interpreted * 2 >= total) { + return TYPE_INTERPRETED; + } else { + return (byte) (key >>> TYPE_SHIFT); } + } - int depth(long cutoff) { - int depth = 0; - if (size() > 0) { - for (Frame child : values()) { - if (child.total >= cutoff) { - depth = Math.max(depth, child.depth(cutoff)); - } - } + int depth(long cutoff) { + int depth = 0; + if (size() > 0) { + for (Frame child : values()) { + if (child.total >= cutoff) { + depth = Math.max(depth, child.depth(cutoff)); } - return depth + 1; + } } + return depth + 1; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java index e7240ee78f8..65b66bf6012 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java @@ -8,41 +8,41 @@ import java.lang.reflect.Array; import java.util.HashMap; -public class Index extends HashMap { - private static final long serialVersionUID = 1L; - private final Class cls; - - public Index(Class cls, T empty) { - this(cls, empty, 256); - } - - public Index(Class cls, T empty, int initialCapacity) { - super(initialCapacity); - this.cls = cls; - super.put(empty, 0); - } - - public int index(T key) { - Integer index = super.get(key); - if (index != null) { - return index; - } else { - int newIndex = super.size(); - super.put(key, newIndex); - return newIndex; - } - } - - @SuppressWarnings("unchecked") - public T[] keys() { - T[] result = (T[]) Array.newInstance(cls, size()); - keys(result); - return result; +public final class Index extends HashMap { + private static final long serialVersionUID = 1L; + private final Class cls; + + public Index(Class cls, T empty) { + this(cls, empty, 256); + } + + public Index(Class cls, T empty, int initialCapacity) { + super(initialCapacity); + this.cls = cls; + super.put(empty, 0); + } + + public int index(T key) { + Integer index = super.get(key); + if (index != null) { + return index; + } else { + int newIndex = super.size(); + super.put(key, newIndex); + return newIndex; } - - public void keys(T[] result) { - for (Entry entry : entrySet()) { - result[entry.getValue()] = entry.getKey(); - } + } + + @SuppressWarnings("unchecked") + public T[] keys() { + T[] result = (T[]) Array.newInstance(cls, size()); + keys(result); + return result; + } + + public void keys(T[] result) { + for (Entry entry : entrySet()) { + result[entry.getValue()] = entry.getKey(); } + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java index 8c011c052ff..bcb64eb5566 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -6,12 +6,10 @@ import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.StackTrace; import io.sentry.protocol.jfr.jfr.event.Event; -import io.sentry.protocol.profiling.JfrFrame; import io.sentry.protocol.profiling.JfrProfile; import io.sentry.protocol.profiling.JfrSample; -//import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +// import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.protocol.profiling.ThreadMetadata; - import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -19,10 +17,10 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Objects; +import org.jetbrains.annotations.NotNull; -public class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { - private final JfrProfile jfrProfile = new JfrProfile(); +public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { + private final @NotNull JfrProfile jfrProfile = new JfrProfile(); public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { super(jfr, args); @@ -30,9 +28,12 @@ public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { public static void main(String[] args) throws IOException { - Path jfrPath = Paths.get("/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); + Path jfrPath = + Paths.get( + "/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); -// JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); + // JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); + System.out.println(profile.frames); System.out.println("Done"); } @@ -41,105 +42,121 @@ 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(jfrProfile.threadMetadata == null) { - jfrProfile.threadMetadata = new HashMap<>(); - } - - - long threadIdToUse = jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; - - jfrProfile.threadMetadata.computeIfAbsent(String.valueOf(threadIdToUse), k -> { - ThreadMetadata metadata = new ThreadMetadata(); - metadata.name = getThreadName(event.tid); - metadata.priority = 0; - return metadata; - }); - } - - if(jfrProfile.samples == null) { - jfrProfile.samples = new ArrayList<>(); - } - - if(jfrProfile.frames == null) { - jfrProfile.frames = new ArrayList<>(); - } - - List stack = new ArrayList<>(); - int currentStack = stacks.size(); - int currentFrame = jfrProfile.frames.size(); - 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]); - 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(""); + 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 (jfrProfile.threadMetadata == null) { + jfrProfile.threadMetadata = new HashMap<>(); + } + + long threadIdToUse = + jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; + + if (jfrProfile.threadMetadata != null) { + jfrProfile.threadMetadata.computeIfAbsent( + String.valueOf(threadIdToUse), + k -> { + ThreadMetadata metadata = new ThreadMetadata(); + metadata.name = getThreadName(event.tid); + metadata.priority = 0; + return metadata; + }); + } + } + + if (jfrProfile.samples == null) { + jfrProfile.samples = new ArrayList<>(); + } + + if (jfrProfile.frames == null) { + jfrProfile.frames = new ArrayList<>(); + } + + List stack = new ArrayList<>(); + int currentStack = stacks.size(); + int currentFrame = jfrProfile.frames != null ? jfrProfile.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]); + 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 (jfrProfile.frames != null) { + jfrProfile.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(event.tid); + sample.threadId = + String.valueOf( + jfr.threads.get(event.tid) != null + ? jfr.javaThreads.get(event.tid) + : event.tid); + sample.stackId = currentStack; + if (jfrProfile.samples != null) { + jfrProfile.samples.add(sample); + } + + stacks.add(stack); } - - 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); - - jfrProfile.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(event.tid); - sample.threadId = String.valueOf(jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid); - sample.stackId = currentStack; - jfrProfile.samples.add(sample); - - stacks.add(stack); - } - } - }); + }); jfrProfile.stacks = stacks; System.out.println("Samples: " + events.size()); } - public static JfrProfile convertFromFile(Path jfrFilePath) throws IOException { + public static @NotNull JfrProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException { JfrAsyncProfilerToSentryProfileConverter converter; try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { Arguments args = new Arguments(); diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java index 1860827478e..e670b46bb52 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java @@ -5,271 +5,281 @@ package io.sentry.protocol.jfr.convert; +import static io.sentry.protocol.jfr.convert.Frame.*; + import io.sentry.protocol.jfr.jfr.ClassRef; import io.sentry.protocol.jfr.jfr.Dictionary; import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.MethodRef; import io.sentry.protocol.jfr.jfr.event.*; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.BitSet; import java.util.Map; - -import static io.sentry.protocol.jfr.convert.Frame.*; +import org.jetbrains.annotations.NotNull; public abstract class JfrConverter extends Classifier { - protected final JfrReader jfr; - protected final Arguments args; - protected final EventCollector collector; - protected Dictionary methodNames; + protected final @NotNull JfrReader jfr; + protected final @NotNull Arguments args; + protected final @NotNull EventCollector collector; + protected @NotNull Dictionary methodNames; - public JfrConverter(JfrReader jfr, Arguments args) { - this.jfr = jfr; - this.args = args; + public JfrConverter(@NotNull JfrReader jfr, @NotNull Arguments args) { + this.jfr = jfr; + this.args = args; + this.methodNames = new Dictionary<>(); - EventCollector collector = createCollector(args); - this.collector = args.nativemem && args.leak ? new MallocLeakAggregator(collector) : collector; - } + EventCollector collector = createCollector(args); + this.collector = args.nativemem && args.leak ? new MallocLeakAggregator(collector) : collector; + } - public void convert() throws IOException { - jfr.stopAtNewChunk = true; + public void convert() throws IOException { + jfr.stopAtNewChunk = true; - while (jfr.hasMoreChunks()) { - // Reset method dictionary, since new chunk may have different IDs - methodNames = new Dictionary<>(); + while (jfr.hasMoreChunks()) { + // Reset method dictionary, since new chunk may have different IDs + methodNames = new Dictionary<>(); - collector.beforeChunk(); - collectEvents(); - collector.afterChunk(); + collector.beforeChunk(); + collectEvents(); + collector.afterChunk(); - convertChunk(); - } - - if (collector.finish()) { - convertChunk(); - } + convertChunk(); } - protected EventCollector createCollector(Arguments args) { - return new EventAggregator(args.threads, args.grain); + if (collector.finish()) { + convertChunk(); } - - protected void collectEvents() throws IOException { - Class eventClass = args.nativemem ? MallocEvent.class - : args.live ? LiveObject.class - : args.alloc ? AllocationSample.class - : args.lock ? ContendedLock.class - : ExecutionSample.class; - - BitSet threadStates = null; - if (args.state != null) { - threadStates = new BitSet(); - for (String state : args.state.toUpperCase().split(",")) { - threadStates.set(toThreadState(state)); - } - } else if (args.cpu) { - threadStates = getThreadStates(true); - } else if (args.wall) { - threadStates = getThreadStates(false); - } - - long startTicks = args.from != 0 ? toTicks(args.from) : Long.MIN_VALUE; - long endTicks = args.to != 0 ? toTicks(args.to) : Long.MAX_VALUE; - - for (Event event; (event = jfr.readEvent(eventClass)) != null; ) { - if (event.time >= startTicks && event.time <= endTicks) { - if (threadStates == null || threadStates.get(((ExecutionSample) event).threadState)) { - collector.collect(event); - } - } - } + } + + protected EventCollector createCollector(Arguments args) { + return new EventAggregator(args.threads, args.grain); + } + + protected void collectEvents() throws IOException { + Class eventClass = + args.nativemem + ? MallocEvent.class + : args.live + ? LiveObject.class + : args.alloc + ? AllocationSample.class + : args.lock ? ContendedLock.class : ExecutionSample.class; + + BitSet threadStates = null; + if (args.state != null) { + threadStates = new BitSet(); + for (String state : args.state.toUpperCase().split(",", -1)) { + threadStates.set(toThreadState(state)); + } + } else if (args.cpu) { + threadStates = getThreadStates(true); + } else if (args.wall) { + threadStates = getThreadStates(false); } - protected void convertChunk() { - // To be overridden in subclasses - } + long startTicks = args.from != 0 ? toTicks(args.from) : Long.MIN_VALUE; + long endTicks = args.to != 0 ? toTicks(args.to) : Long.MAX_VALUE; - protected int toThreadState(String name) { - Map threadStates = jfr.enums.get("jdk.types.ThreadState"); - if (threadStates != null) { - for (Map.Entry entry : threadStates.entrySet()) { - if (entry.getValue().startsWith(name, 6)) { - return entry.getKey(); - } - } + for (Event event; (event = jfr.readEvent(eventClass)) != null; ) { + if (event.time >= startTicks && event.time <= endTicks) { + if (threadStates == null || threadStates.get(((ExecutionSample) event).threadState)) { + collector.collect(event); } - throw new IllegalArgumentException("Unknown thread state: " + name); + } } - - protected BitSet getThreadStates(boolean cpu) { - BitSet set = new BitSet(); - Map threadStates = jfr.enums.get("jdk.types.ThreadState"); - if (threadStates != null) { - for (Map.Entry entry : threadStates.entrySet()) { - set.set(entry.getKey(), "STATE_DEFAULT".equals(entry.getValue()) == cpu); - } + } + + protected void convertChunk() { + // To be overridden in subclasses + } + + protected int toThreadState(String name) { + Map threadStates = jfr.enums.get("jdk.types.ThreadState"); + if (threadStates != null) { + for (Map.Entry entry : threadStates.entrySet()) { + if (entry.getValue().startsWith(name, 6)) { + return entry.getKey(); } - return set; + } } - - // millis can be an absolute timestamp or an offset from the beginning/end of the recording - protected long toTicks(long millis) { - long nanos = millis * 1_000_000; - if (millis < 0) { - nanos += jfr.endNanos; - } else if (millis < 1500000000000L) { - nanos += jfr.startNanos; - } - return (long) ((nanos - jfr.chunkStartNanos) * (jfr.ticksPerSec / 1e9)) + jfr.chunkStartTicks; + throw new IllegalArgumentException("Unknown thread state: " + name); + } + + protected BitSet getThreadStates(boolean cpu) { + BitSet set = new BitSet(); + Map threadStates = jfr.enums.get("jdk.types.ThreadState"); + if (threadStates != null) { + for (Map.Entry entry : threadStates.entrySet()) { + set.set(entry.getKey(), "STATE_DEFAULT".equals(entry.getValue()) == cpu); + } } - - @Override - public String getMethodName(long methodId, byte methodType) { - String result = methodNames.get(methodId); - if (result == null) { - methodNames.put(methodId, result = resolveMethodName(methodId, methodType)); - } - return result; + return set; + } + + // millis can be an absolute timestamp or an offset from the beginning/end of the recording + protected long toTicks(long millis) { + long nanos = millis * 1_000_000; + if (millis < 0) { + nanos += jfr.endNanos; + } else if (millis < 1500000000000L) { + nanos += jfr.startNanos; } - - private String resolveMethodName(long methodId, byte methodType) { - MethodRef method = jfr.methods.get(methodId); - if (method == null) { - return "unknown"; - } - - ClassRef cls = jfr.classes.get(method.cls); - byte[] className = jfr.symbols.get(cls.name); - byte[] methodName = jfr.symbols.get(method.name); - - if (className == null || className.length == 0 || isNativeFrame(methodType)) { - return new String(methodName, StandardCharsets.UTF_8); - } else { - String classStr = toJavaClassName(className, 0, args.dot); - if (methodName == null || methodName.length == 0) { - return classStr; - } - String methodStr = new String(methodName, StandardCharsets.UTF_8); - return classStr + '.' + methodStr; - } + return (long) ((nanos - jfr.chunkStartNanos) * (jfr.ticksPerSec / 1e9)) + jfr.chunkStartTicks; + } + + @Override + public String getMethodName(long methodId, byte methodType) { + String result = methodNames.get(methodId); + if (result == null) { + methodNames.put(methodId, result = resolveMethodName(methodId, methodType)); } + return result; + } - public String getClassName(long classId) { - ClassRef cls = jfr.classes.get(classId); - if (cls == null) { - return "null"; - } - byte[] className = jfr.symbols.get(cls.name); + private String resolveMethodName(long methodId, byte methodType) { + MethodRef method = jfr.methods.get(methodId); + if (method == null) { + return "unknown"; + } - int arrayDepth = 0; - while (className[arrayDepth] == '[') { - arrayDepth++; - } + ClassRef cls = jfr.classes.get(method.cls); + byte[] className = jfr.symbols.get(cls.name); + byte[] methodName = jfr.symbols.get(method.name); + + if (className == null || className.length == 0 || isNativeFrame(methodType)) { + return new String(methodName, StandardCharsets.UTF_8); + } else { + String classStr = toJavaClassName(className, 0, args.dot); + if (methodName == null || methodName.length == 0) { + return classStr; + } + String methodStr = new String(methodName, StandardCharsets.UTF_8); + return classStr + '.' + methodStr; + } + } - String name = toJavaClassName(className, arrayDepth, true); - while (arrayDepth-- > 0) { - name = name.concat("[]"); - } - return name; + public String getClassName(long classId) { + ClassRef cls = jfr.classes.get(classId); + if (cls == null) { + return "null"; } + byte[] className = jfr.symbols.get(cls.name); - private String toJavaClassName(byte[] symbol, int start, boolean dotted) { - int end = symbol.length; - if (start > 0) { - switch (symbol[start]) { - case 'B': - return "byte"; - case 'C': - return "char"; - case 'S': - return "short"; - case 'I': - return "int"; - case 'J': - return "long"; - case 'Z': - return "boolean"; - case 'F': - return "float"; - case 'D': - return "double"; - case 'L': - start++; - end--; - } - } + int arrayDepth = 0; + while (className[arrayDepth] == '[') { + arrayDepth++; + } - if (args.norm) { - for (int i = end - 2; i > start; i--) { - if (symbol[i] == '/' || symbol[i] == '.') { - if (symbol[i + 1] >= '0' && symbol[i + 1] <= '9') { - end = i; - if (i > start + 19 && symbol[i - 19] == '+' && symbol[i - 18] == '0') { - // Original JFR transforms lambda names to something like - // pkg.ClassName$$Lambda+0x00007f8177090218/543846639 - end = i - 19; - } - } - break; - } - } - } + String name = toJavaClassName(className, arrayDepth, true); + while (arrayDepth-- > 0) { + name = name.concat("[]"); + } + return name; + } + + private String toJavaClassName(byte[] symbol, int start, boolean dotted) { + int end = symbol.length; + if (start > 0) { + switch (symbol[start]) { + case 'B': + return "byte"; + case 'C': + return "char"; + case 'S': + return "short"; + case 'I': + return "int"; + case 'J': + return "long"; + case 'Z': + return "boolean"; + case 'F': + return "float"; + case 'D': + return "double"; + case 'L': + start++; + end--; + } + } - if (args.simple) { - for (int i = end - 2; i >= start; i--) { - if (symbol[i] == '/' && (symbol[i + 1] < '0' || symbol[i + 1] > '9')) { - start = i + 1; - break; - } + if (args.norm) { + for (int i = end - 2; i > start; i--) { + if (symbol[i] == '/' || symbol[i] == '.') { + if (symbol[i + 1] >= '0' && symbol[i + 1] <= '9') { + end = i; + if (i > start + 19 && symbol[i - 19] == '+' && symbol[i - 18] == '0') { + // Original JFR transforms lambda names to something like + // pkg.ClassName$$Lambda+0x00007f8177090218/543846639 + end = i - 19; } + } + break; } - - String s = new String(symbol, start, end - start, StandardCharsets.UTF_8); - return dotted ? s.replace('/', '.') : s; + } } - public StackTraceElement getStackTraceElement(long methodId, byte methodType, int location) { - MethodRef method = jfr.methods.get(methodId); - if (method == null) { - return new StackTraceElement("", "unknown", null, 0); + if (args.simple) { + for (int i = end - 2; i >= start; i--) { + if (symbol[i] == '/' && (symbol[i + 1] < '0' || symbol[i + 1] > '9')) { + start = i + 1; + break; } - - ClassRef cls = jfr.classes.get(method.cls); - byte[] className = jfr.symbols.get(cls.name); - byte[] methodName = jfr.symbols.get(method.name); - - String classStr = className == null || className.length == 0 || isNativeFrame(methodType) ? "" : - toJavaClassName(className, 0, args.dot); - String methodStr = methodName == null || methodName.length == 0 ? "" : - new String(methodName, StandardCharsets.UTF_8); - return new StackTraceElement(classStr, methodStr, null, location >>> 16); + } } - public String getThreadName(int tid) { - String threadName = jfr.threads.get(tid); - return threadName == null ? "[tid=" + tid + ']' : - threadName.startsWith("[tid=") ? threadName : '[' + threadName + " tid=" + tid + ']'; - } + String s = new String(symbol, start, end - start, StandardCharsets.UTF_8); + return dotted ? s.replace('/', '.') : s; + } - protected boolean isNativeFrame(byte methodType) { - // In JDK Flight Recorder, TYPE_NATIVE denotes Java native methods, - // while in async-profiler, TYPE_NATIVE is for C methods - return methodType == TYPE_NATIVE && jfr.getEnumValue("jdk.types.FrameType", TYPE_KERNEL) != null || - methodType == TYPE_CPP || - methodType == TYPE_KERNEL; + public StackTraceElement getStackTraceElement(long methodId, byte methodType, int location) { + MethodRef method = jfr.methods.get(methodId); + if (method == null) { + return new StackTraceElement("", "unknown", null, 0); } - // Select sum(samples) or sum(value) depending on the --total option. - // For lock events, convert lock duration from ticks to nanoseconds. - protected abstract class AggregatedEventVisitor implements EventCollector.Visitor { - final double factor = !args.total ? 0.0 : args.lock ? 1e9 / jfr.ticksPerSec : 1.0; - - @Override - public final void visit(Event event, long samples, long value) { - visit(event, factor == 0.0 ? samples : factor == 1.0 ? value : (long) (value * factor)); - } + ClassRef cls = jfr.classes.get(method.cls); + byte[] className = jfr.symbols.get(cls.name); + byte[] methodName = jfr.symbols.get(method.name); + + String classStr = + className == null || className.length == 0 || isNativeFrame(methodType) + ? "" + : toJavaClassName(className, 0, args.dot); + String methodStr = + methodName == null || methodName.length == 0 + ? "" + : new String(methodName, StandardCharsets.UTF_8); + return new StackTraceElement(classStr, methodStr, null, location >>> 16); + } + + public String getThreadName(int tid) { + String threadName = jfr.threads.get(tid); + return threadName == null + ? "[tid=" + tid + ']' + : threadName.startsWith("[tid=") ? threadName : '[' + threadName + " tid=" + tid + ']'; + } + + protected boolean isNativeFrame(byte methodType) { + // In JDK Flight Recorder, TYPE_NATIVE denotes Java native methods, + // while in async-profiler, TYPE_NATIVE is for C methods + return (methodType == TYPE_NATIVE + && jfr.getEnumValue("jdk.types.FrameType", TYPE_KERNEL) != null) + || methodType == TYPE_CPP + || methodType == TYPE_KERNEL; + } + + // Select sum(samples) or sum(value) depending on the --total option. + // For lock events, convert lock duration from ticks to nanoseconds. + protected abstract class AggregatedEventVisitor implements EventCollector.Visitor { + final double factor = !args.total ? 0.0 : args.lock ? 1e9 / jfr.ticksPerSec : 1.0; - protected abstract void visit(Event event, long value); + @Override + public final void visit(Event event, long samples, long value) { + visit(event, factor == 0.0 ? samples : factor == 1.0 ? value : (long) (value * factor)); } + + protected abstract void visit(Event event, long value); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java index 469f0979aed..d8f13e746d3 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java @@ -5,87 +5,90 @@ package io.sentry.protocol.jfr.convert; +import static io.sentry.protocol.jfr.convert.Frame.*; + import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.StackTrace; import io.sentry.protocol.jfr.jfr.event.AllocationSample; import io.sentry.protocol.jfr.jfr.event.Event; - import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; -import static io.sentry.protocol.jfr.convert.Frame.*; - -/** - * Converts .jfr output to HTML Flame Graph. - */ -public class JfrToFlame extends JfrConverter { - private final FlameGraph fg; - - public JfrToFlame(JfrReader jfr, Arguments args) { - super(jfr, args); - this.fg = new FlameGraph(args); - } +/** Converts .jfr output to HTML Flame Graph. */ +public final class JfrToFlame extends JfrConverter { + private final FlameGraph fg; - @Override - protected void convertChunk() { - collector.forEach(new AggregatedEventVisitor() { - final CallStack stack = new CallStack(); + public JfrToFlame(JfrReader jfr, Arguments args) { + super(jfr, args); + this.fg = new FlameGraph(args); + } - @Override - public void visit(Event event, long value) { - StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); - if (stackTrace != null) { - Arguments args = JfrToFlame.this.args; - long[] methods = stackTrace.methods; - byte[] types = stackTrace.types; - int[] locations = stackTrace.locations; + @Override + protected void convertChunk() { + collector.forEach( + new AggregatedEventVisitor() { + final CallStack stack = new CallStack(); - if (args.threads) { - stack.push(getThreadName(event.tid), TYPE_NATIVE); - } - if (args.classify) { - Classifier.Category category = getCategory(stackTrace); - stack.push(category.title, category.type); - } - for (int i = methods.length; --i >= 0; ) { - String methodName = getMethodName(methods[i], types[i]); - int location; - if (args.lines && (location = locations[i] >>> 16) != 0) { - methodName += ":" + location; - } else if (args.bci && (location = locations[i] & 0xffff) != 0) { - methodName += "@" + location; - } - stack.push(methodName, types[i]); - } - long classId = event.classId(); - if (classId != 0) { - stack.push(getClassName(classId), (event instanceof AllocationSample) - && ((AllocationSample) event).tlabSize == 0 ? TYPE_KERNEL : TYPE_INLINED); - } + @Override + public void visit(Event event, long value) { + StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + if (stackTrace != null) { + Arguments args = JfrToFlame.this.args; + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; + int[] locations = stackTrace.locations; - fg.addSample(stack, value); - stack.clear(); + if (args.threads) { + stack.push(getThreadName(event.tid), TYPE_NATIVE); + } + if (args.classify) { + Classifier.Category category = getCategory(stackTrace); + if (category != null) { + stack.push(category.title, category.type); + } + } + for (int i = methods.length; --i >= 0; ) { + String methodName = getMethodName(methods[i], types[i]); + int location; + if (args.lines && (location = locations[i] >>> 16) != 0) { + methodName += ":" + location; + } else if (args.bci && (location = locations[i] & 0xffff) != 0) { + methodName += "@" + location; } + stack.push(methodName, types[i]); + } + long classId = event.classId(); + if (classId != 0) { + stack.push( + getClassName(classId), + (event instanceof AllocationSample) && ((AllocationSample) event).tlabSize == 0 + ? TYPE_KERNEL + : TYPE_INLINED); + } + + fg.addSample(stack, value); + stack.clear(); } + } }); - } + } - public void dump(OutputStream out) throws IOException { - try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { - fg.dump(ps); - } + public void dump(OutputStream out) throws IOException { + try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { + fg.dump(ps); } + } - public static void convert(String input, String output, Arguments args) throws IOException { - JfrToFlame converter; - try (JfrReader jfr = new JfrReader(input)) { - converter = new JfrToFlame(jfr, args); - converter.convert(); - } - try (FileOutputStream out = new FileOutputStream(output)) { - converter.dump(out); - } + public static void convert(String input, String output, Arguments args) throws IOException { + JfrToFlame converter; + try (JfrReader jfr = new JfrReader(input)) { + converter = new JfrToFlame(jfr, args); + converter.convert(); + } + try (FileOutputStream out = new FileOutputStream(output)) { + converter.dump(out); } + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java index b6f08ac0a81..7e061ded7a1 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java @@ -10,29 +10,28 @@ import java.io.InputStream; import java.io.PrintStream; -public class ResourceProcessor { +public final class ResourceProcessor { - public static String getResource(String name) { - try (InputStream stream = ResourceProcessor.class.getResourceAsStream(name)) { - if (stream == null) { - throw new IOException("No resource found"); - } + public static String getResource(String name) { + try (InputStream stream = ResourceProcessor.class.getResourceAsStream(name)) { + if (stream == null) { + throw new IOException("No resource found"); + } - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[32768]; - for (int length; (length = stream.read(buffer)) != -1; ) { - result.write(buffer, 0, length); - } - return result.toString("UTF-8"); - } catch (IOException e) { - throw new IllegalStateException("Can't load resource with name " + name); - } - } - - public static String printTill(PrintStream out, String data, String till) { - int index = data.indexOf(till); - out.print(data.substring(0, index)); - return data.substring(index + till.length()); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[32768]; + for (int length; (length = stream.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + return result.toString("UTF-8"); + } catch (IOException e) { + throw new IllegalStateException("Can't load resource with name " + name); } + } + public static String printTill(PrintStream out, String data, String till) { + int index = data.indexOf(till); + out.print(data.substring(0, index)); + return data.substring(index + till.length()); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java index 6367830edce..78e0fbfb577 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java @@ -5,10 +5,10 @@ package io.sentry.protocol.jfr.jfr; -public class ClassRef { - public final long name; +public final class ClassRef { + public final long name; - public ClassRef(long name) { - this.name = name; - } + public ClassRef(long name) { + this.name = name; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java index c903a69e684..47438e3833e 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java @@ -7,110 +7,108 @@ import java.util.Arrays; -/** - * Fast and compact long->Object map. - */ -public class Dictionary { - private static final int INITIAL_CAPACITY = 16; - - private long[] keys; - private Object[] values; - private int size; - - public Dictionary() { - this(INITIAL_CAPACITY); +/** Fast and compact long->Object map. */ +public final class Dictionary { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private Object[] values; + private int size; + + public Dictionary() { + this(INITIAL_CAPACITY); + } + + public Dictionary(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new Object[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, null); + size = 0; + } + + public int size() { + return size; + } + + public void put(long key, T value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); } - public Dictionary(int initialCapacity) { - this.keys = new long[initialCapacity]; - this.values = new Object[initialCapacity]; - } - - public void clear() { - Arrays.fill(keys, 0); - Arrays.fill(values, null); - size = 0; + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; } + keys[i] = key; + values[i] = value; - public int size() { - return size; + if (++size * 2 > keys.length) { + resize(keys.length * 2); } - - public void put(long key, T value) { - if (key == 0) { - throw new IllegalArgumentException("Zero key not allowed"); - } - - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != 0) { - if (keys[i] == key) { - values[i] = value; - return; - } - i = (i + 1) & mask; - } - keys[i] = key; - values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } + } + + @SuppressWarnings("unchecked") + public T get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key && keys[i] != 0) { + i = (i + 1) & mask; } - - @SuppressWarnings("unchecked") - public T get(long key) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key && keys[i] != 0) { - i = (i + 1) & mask; - } - return (T) values[i]; + return (T) values[i]; + } + + @SuppressWarnings("unchecked") + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], (T) values[i]); + } } + } - @SuppressWarnings("unchecked") - public void forEach(Visitor visitor) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - visitor.visit(keys[i], (T) values[i]); - } - } + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); } - - public int preallocate(int count) { - if (count * 2 > keys.length) { - resize(Integer.highestOneBit(count * 4 - 1)); + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + Object[] newValues = new Object[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } } - return count; + } } - private void resize(int newCapacity) { - long[] newKeys = new long[newCapacity]; - Object[] newValues = new Object[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == 0) { - newKeys[j] = keys[i]; - newValues[j] = values[i]; - break; - } - } - } - } + keys = newKeys; + values = newValues; + } - keys = newKeys; - values = newValues; - } + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } - private static int hashCode(long key) { - key *= 0xc6a4a7935bd1e995L; - return (int) (key ^ (key >>> 32)); - } - - public interface Visitor { - void visit(long key, T value); - } + public interface Visitor { + void visit(long key, T value); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java index aec9b7b624f..0543a74218e 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java @@ -7,119 +7,117 @@ import java.util.Arrays; -/** - * Fast and compact long->int map. - */ -public class DictionaryInt { - private static final int INITIAL_CAPACITY = 16; - - private long[] keys; - private int[] values; - private int size; - - public DictionaryInt() { - this(INITIAL_CAPACITY); +/** Fast and compact long->int map. */ +public final class DictionaryInt { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private int[] values; + private int size; + + public DictionaryInt() { + this(INITIAL_CAPACITY); + } + + public DictionaryInt(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new int[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, 0); + size = 0; + } + + public void put(long key, int value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); } - public DictionaryInt(int initialCapacity) { - this.keys = new long[initialCapacity]; - this.values = new int[initialCapacity]; + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; } + keys[i] = key; + values[i] = value; - public void clear() { - Arrays.fill(keys, 0); - Arrays.fill(values, 0); - size = 0; + if (++size * 2 > keys.length) { + resize(keys.length * 2); } - - public void put(long key, int value) { - if (key == 0) { - throw new IllegalArgumentException("Zero key not allowed"); - } - - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != 0) { - if (keys[i] == key) { - values[i] = value; - return; - } - i = (i + 1) & mask; - } - keys[i] = key; - values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } + } + + public int get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + throw new IllegalArgumentException("No such key: " + key); + } + i = (i + 1) & mask; } - - public int get(long key) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key) { - if (keys[i] == 0) { - throw new IllegalArgumentException("No such key: " + key); - } - i = (i + 1) & mask; - } - return values[i]; + return values[i]; + } + + public int get(long key, int notFound) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + return notFound; + } + i = (i + 1) & mask; } - - public int get(long key, int notFound) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key) { - if (keys[i] == 0) { - return notFound; - } - i = (i + 1) & mask; - } - return values[i]; + return values[i]; + } + + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], values[i]); + } } + } - public void forEach(Visitor visitor) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - visitor.visit(keys[i], values[i]); - } - } + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); } - - public int preallocate(int count) { - if (count * 2 > keys.length) { - resize(Integer.highestOneBit(count * 4 - 1)); + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + int[] newValues = new int[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } } - return count; + } } - private void resize(int newCapacity) { - long[] newKeys = new long[newCapacity]; - int[] newValues = new int[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == 0) { - newKeys[j] = keys[i]; - newValues[j] = values[i]; - break; - } - } - } - } + keys = newKeys; + values = newValues; + } - keys = newKeys; - values = newValues; - } + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } - private static int hashCode(long key) { - key *= 0xc6a4a7935bd1e995L; - return (int) (key ^ (key >>> 32)); - } - - public interface Visitor { - void visit(long key, int value); - } + public interface Visitor { + void visit(long key, int value); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java index d814026a84c..ac7772222e3 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java @@ -5,8 +5,11 @@ package io.sentry.protocol.jfr.jfr; -class Element { +abstract class Element { - void addChild(Element e) { - } + void addChild(Element e) {} + + static final class NoOpElement extends Element { + // Empty implementation for unhandled element types + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java index fbdbc521357..6cbb16259f5 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java @@ -8,33 +8,35 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public class JfrClass extends Element { - final int id; - final boolean simpleType; - final String name; - final List fields; +public final class JfrClass extends Element { + final int id; + final boolean simpleType; + final @Nullable String name; + final List fields; - JfrClass(Map attributes) { - this.id = Integer.parseInt(attributes.get("id")); - this.simpleType = "true".equals(attributes.get("simpleType")); - this.name = attributes.get("name"); - this.fields = new ArrayList<>(2); - } + JfrClass(@NotNull Map attributes) { + this.id = Integer.parseInt(attributes.get("id")); + this.simpleType = "true".equals(attributes.get("simpleType")); + this.name = attributes.get("name"); + this.fields = new ArrayList<>(2); + } - @Override - void addChild(Element e) { - if (e instanceof JfrField) { - fields.add((JfrField) e); - } + @Override + void addChild(Element e) { + if (e instanceof JfrField) { + fields.add((JfrField) e); } + } - public JfrField field(String name) { - for (JfrField field : fields) { - if (field.name.equals(name)) { - return field; - } - } - return null; + public @Nullable JfrField field(@NotNull String name) { + for (JfrField field : fields) { + if (field.name != null && field.name.equals(name)) { + return field; + } } + return null; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java index a96f5555e5b..3c9dc040700 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java @@ -6,15 +6,17 @@ package io.sentry.protocol.jfr.jfr; import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public class JfrField extends Element { - final String name; - final int type; - final boolean constantPool; +public final class JfrField extends Element { + final @Nullable String name; + final int type; + final boolean constantPool; - JfrField(Map attributes) { - this.name = attributes.get("name"); - this.type = Integer.parseInt(attributes.get("class")); - this.constantPool = "true".equals(attributes.get("constantPool")); - } + JfrField(@NotNull Map attributes) { + this.name = attributes.get("name"); + this.type = Integer.parseInt(attributes.get("class")); + this.constantPool = "true".equals(attributes.get("constantPool")); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java index ecae4b1b4db..cc6f73cdf95 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java @@ -6,7 +6,6 @@ package io.sentry.protocol.jfr.jfr; import io.sentry.protocol.jfr.jfr.event.*; - import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Constructor; @@ -21,667 +20,683 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - -/** - * Parses JFR output produced by async-profiler. - */ -public class JfrReader implements Closeable { - private static final int BUFFER_SIZE = 2 * 1024 * 1024; - private static final int CHUNK_HEADER_SIZE = 68; - private static final int CHUNK_SIGNATURE = 0x464c5200; - - private static final byte STATE_NEW_CHUNK = 0; - private static final byte STATE_READING = 1; - private static final byte STATE_EOF = 2; - private static final byte STATE_INCOMPLETE = 3; - - private final FileChannel ch; - private ByteBuffer buf; - private final long fileSize; - private long filePosition; - private byte state; - - public long startNanos = Long.MAX_VALUE; - public long endNanos = Long.MIN_VALUE; - public long startTicks = Long.MAX_VALUE; - public long chunkStartNanos; - public long chunkEndNanos; - public long chunkStartTicks; - public long ticksPerSec; - public boolean stopAtNewChunk; - - public final Dictionary types = new Dictionary<>(); - public final Map typesByName = new HashMap<>(); - public final Dictionary threads = new Dictionary<>(); - public final Dictionary javaThreads = new Dictionary<>(); - public final Dictionary classes = new Dictionary<>(); - public final Dictionary strings = new Dictionary<>(); - public final Dictionary symbols = new Dictionary<>(); - public final Dictionary methods = new Dictionary<>(); - public final Dictionary stackTraces = new Dictionary<>(); - public final Map settings = new HashMap<>(); - public final Map> enums = new HashMap<>(); - - private final Dictionary> customEvents = new Dictionary<>(); - - private int executionSample; - private int nativeMethodSample; - private int wallClockSample; - private int allocationInNewTLAB; - private int allocationOutsideTLAB; - private int allocationSample; - private int liveObject; - private int monitorEnter; - private int threadPark; - private int activeSetting; - private int malloc; - private int free; - - public JfrReader(String fileName) throws IOException { - this.ch = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ); - this.buf = ByteBuffer.allocateDirect(BUFFER_SIZE); - this.fileSize = ch.size(); - - buf.flip(); - ensureBytes(CHUNK_HEADER_SIZE); - if (!readChunk(0)) { - throw new IOException("Incomplete JFR file"); - } - } - - public JfrReader(ByteBuffer buf) throws IOException { - this.ch = null; - this.buf = buf; - this.fileSize = buf.limit(); - - buf.order(ByteOrder.BIG_ENDIAN); - if (!readChunk(0)) { - throw new IOException("Incomplete JFR file"); - } - } - - @Override - public void close() throws IOException { - if (ch != null) { - ch.close(); - } - } - - public boolean eof() { - return state >= STATE_EOF; - } - - public boolean incomplete() { - return state == STATE_INCOMPLETE; - } - - public long durationNanos() { - return endNanos - startNanos; - } - - public void registerEvent(String name, Class eventClass) { - JfrClass type = typesByName.get(name); - if (type != null) { - try { - customEvents.put(type.id, eventClass.getConstructor(JfrReader.class)); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("No suitable constructor found"); - } - } - } - - // Similar to eof(), but parses the next chunk header - public boolean hasMoreChunks() throws IOException { - return state == STATE_NEW_CHUNK ? readChunk(buf.position()) : state == STATE_READING; - } - - public List readAllEvents() throws IOException { - return readAllEvents(null); - } - - public List readAllEvents(Class cls) throws IOException { - ArrayList events = new ArrayList<>(); - for (E event; (event = readEvent(cls)) != null; ) { - events.add(event); +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Parses JFR output produced by async-profiler. */ +public final class JfrReader implements Closeable { + private static final int BUFFER_SIZE = 2 * 1024 * 1024; + private static final int CHUNK_HEADER_SIZE = 68; + private static final int CHUNK_SIGNATURE = 0x464c5200; + + private static final byte STATE_NEW_CHUNK = 0; + private static final byte STATE_READING = 1; + private static final byte STATE_EOF = 2; + private static final byte STATE_INCOMPLETE = 3; + + private final @Nullable FileChannel ch; + private @NotNull ByteBuffer buf; + private final long fileSize; + private long filePosition; + private byte state; + + public long startNanos = Long.MAX_VALUE; + public long endNanos = Long.MIN_VALUE; + public long startTicks = Long.MAX_VALUE; + public long chunkStartNanos; + public long chunkEndNanos; + public long chunkStartTicks; + public long ticksPerSec; + public boolean stopAtNewChunk; + + public final Dictionary types = new Dictionary<>(); + public final Map typesByName = new HashMap<>(); + public final Dictionary threads = new Dictionary<>(); + public final Dictionary javaThreads = new Dictionary<>(); + public final Dictionary classes = new Dictionary<>(); + public final Dictionary strings = new Dictionary<>(); + public final Dictionary symbols = new Dictionary<>(); + public final Dictionary methods = new Dictionary<>(); + public final Dictionary stackTraces = new Dictionary<>(); + public final Map settings = new HashMap<>(); + public final Map> enums = new HashMap<>(); + + private final Dictionary> customEvents = new Dictionary<>(); + + private int executionSample; + private int nativeMethodSample; + private int wallClockSample; + private int allocationInNewTLAB; + private int allocationOutsideTLAB; + private int allocationSample; + private int liveObject; + private int monitorEnter; + private int threadPark; + private int activeSetting; + private int malloc; + private int free; + + public JfrReader(String fileName) throws IOException { + this.ch = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ); + this.buf = ByteBuffer.allocateDirect(BUFFER_SIZE); + this.fileSize = ch.size(); + + buf.flip(); + ensureBytes(CHUNK_HEADER_SIZE); + if (!readChunk(0)) { + throw new IOException("Incomplete JFR file"); + } + } + + public JfrReader(@NotNull ByteBuffer buf) throws IOException { + this.ch = null; + this.buf = buf; + this.fileSize = buf.limit(); + + buf.order(ByteOrder.BIG_ENDIAN); + if (!readChunk(0)) { + throw new IOException("Incomplete JFR file"); + } + } + + @Override + public void close() throws IOException { + if (ch != null) { + ch.close(); + } + } + + public boolean eof() { + return state >= STATE_EOF; + } + + public boolean incomplete() { + return state == STATE_INCOMPLETE; + } + + public long durationNanos() { + return endNanos - startNanos; + } + + public void registerEvent(String name, Class eventClass) { + JfrClass type = typesByName.get(name); + if (type != null) { + try { + customEvents.put(type.id, eventClass.getConstructor(JfrReader.class)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("No suitable constructor found"); + } + } + } + + // Similar to eof(), but parses the next chunk header + public boolean hasMoreChunks() throws IOException { + return state == STATE_NEW_CHUNK ? readChunk(buf.position()) : state == STATE_READING; + } + + public List readAllEvents() throws IOException { + return readAllEvents(null); + } + + public List readAllEvents(@Nullable Class cls) throws IOException { + ArrayList events = new ArrayList<>(); + for (E event; (event = readEvent(cls)) != null; ) { + events.add(event); + } + Collections.sort(events); + return events; + } + + public @Nullable Event readEvent() throws IOException { + return readEvent(null); + } + + @SuppressWarnings("unchecked") + public @Nullable E readEvent(@Nullable Class cls) throws IOException { + while (ensureBytes(CHUNK_HEADER_SIZE)) { + int pos = buf.position(); + int size = getVarint(); + int type = getVarint(); + + if (type == 'L' && buf.getInt(pos) == CHUNK_SIGNATURE) { + if (state != STATE_NEW_CHUNK && stopAtNewChunk) { + buf.position(pos); + state = STATE_NEW_CHUNK; + } else if (readChunk(pos)) { + continue; } - Collections.sort(events); - return events; - } - - public Event readEvent() throws IOException { - return readEvent(null); - } - - @SuppressWarnings("unchecked") - public E readEvent(Class cls) throws IOException { - while (ensureBytes(CHUNK_HEADER_SIZE)) { - int pos = buf.position(); - int size = getVarint(); - int type = getVarint(); - - if (type == 'L' && buf.getInt(pos) == CHUNK_SIGNATURE) { - if (state != STATE_NEW_CHUNK && stopAtNewChunk) { - buf.position(pos); - state = STATE_NEW_CHUNK; - } else if (readChunk(pos)) { - continue; - } - return null; - } - - if (type == executionSample || type == nativeMethodSample) { - if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(false); - } else if (type == wallClockSample) { - if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(true); - } else if (type == allocationInNewTLAB) { - if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(true); - } else if (type == allocationOutsideTLAB || type == allocationSample) { - if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(false); - } else if (type == malloc) { - if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(true); - } else if (type == free) { - if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(false); - } else if (type == liveObject) { - if (cls == null || cls == LiveObject.class) return (E) readLiveObject(); - } else if (type == monitorEnter) { - if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(false); - } else if (type == threadPark) { - if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(true); - } else if (type == activeSetting) { - readActiveSetting(); - } else { - Constructor customEvent = customEvents.get(type); - if (customEvent != null && (cls == null || cls == customEvent.getDeclaringClass())) { - try { - return (E) customEvent.newInstance(this); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException(e); - } finally { - seek(filePosition + pos + size); - } - } - } - - seek(filePosition + pos + size); - } - - state = STATE_EOF; return null; - } - - private ExecutionSample readExecutionSample(boolean hasSamples) { - long time = getVarlong(); - int tid = getVarint(); - int stackTraceId = getVarint(); - int threadState = getVarint(); - int samples = hasSamples ? getVarint() : 1; - return new ExecutionSample(time, tid, stackTraceId, threadState, samples); - } - - private AllocationSample readAllocationSample(boolean tlab) { - long time = getVarlong(); - int tid = getVarint(); - int stackTraceId = getVarint(); - int classId = getVarint(); - long allocationSize = getVarlong(); - long tlabSize = tlab ? getVarlong() : 0; - return new AllocationSample(time, tid, stackTraceId, classId, allocationSize, tlabSize); - } - - private MallocEvent readMallocEvent(boolean hasSize) { - long time = getVarlong(); - int tid = getVarint(); - int stackTraceId = getVarint(); - long address = getVarlong(); - long size = hasSize ? getVarlong() : 0; - return new MallocEvent(time, tid, stackTraceId, address, size); - } - - private LiveObject readLiveObject() { - long time = getVarlong(); - int tid = getVarint(); - int stackTraceId = getVarint(); - int classId = getVarint(); - long allocationSize = getVarlong(); - long allocatimeTime = getVarlong(); - return new LiveObject(time, tid, stackTraceId, classId, allocationSize, allocatimeTime); - } - - private ContendedLock readContendedLock(boolean hasTimeout) { - long time = getVarlong(); - long duration = getVarlong(); - int tid = getVarint(); - int stackTraceId = getVarint(); - int classId = getVarint(); - if (hasTimeout) getVarlong(); - long until = getVarlong(); - long address = getVarlong(); - return new ContendedLock(time, tid, stackTraceId, duration, classId); - } - - private void readActiveSetting() { - for (JfrField field : typesByName.get("jdk.ActiveSetting").fields) { - getVarlong(); - if ("id".equals(field.name)) { - break; - } - } - String name = getString(); - String value = getString(); - settings.put(name, value); - } - - private boolean readChunk(int pos) throws IOException { - if (pos + CHUNK_HEADER_SIZE > buf.limit() || buf.getInt(pos) != CHUNK_SIGNATURE) { - throw new IOException("Not a valid JFR file"); - } - - int version = buf.getInt(pos + 4); - if (version < 0x20000 || version > 0x2ffff) { - throw new IOException("Unsupported JFR version: " + (version >>> 16) + "." + (version & 0xffff)); - } - - long chunkStart = filePosition + pos; - long chunkSize = buf.getLong(pos + 8); - if (chunkStart + chunkSize > fileSize) { - state = STATE_INCOMPLETE; - return false; - } - - long cpOffset = buf.getLong(pos + 16); - long metaOffset = buf.getLong(pos + 24); - if (cpOffset == 0 || metaOffset == 0) { - state = STATE_INCOMPLETE; - return false; - } - - chunkStartNanos = buf.getLong(pos + 32); - chunkEndNanos = buf.getLong(pos + 32) + buf.getLong(pos + 40); - chunkStartTicks = buf.getLong(pos + 48); - ticksPerSec = buf.getLong(pos + 56); - - startNanos = Math.min(startNanos, chunkStartNanos); - endNanos = Math.max(endNanos, chunkEndNanos); - startTicks = Math.min(startTicks, chunkStartTicks); - - types.clear(); - typesByName.clear(); - - readMeta(chunkStart + metaOffset); - readConstantPool(chunkStart + cpOffset); - cacheEventTypes(); - - seek(chunkStart + CHUNK_HEADER_SIZE); - state = STATE_READING; - return true; - } - - private void readMeta(long metaOffset) throws IOException { - seek(metaOffset); - ensureBytes(5); - - int posBeforeSize = buf.position(); - ensureBytes(getVarint() - (buf.position() - posBeforeSize)); - getVarint(); - getVarlong(); - getVarlong(); - getVarlong(); - - String[] strings = new String[getVarint()]; - for (int i = 0; i < strings.length; i++) { - strings[i] = getString(); - } - readElement(strings); - } - - private Element readElement(String[] strings) { - String name = strings[getVarint()]; - - int attributeCount = getVarint(); - Map attributes = new HashMap<>(attributeCount); - for (int i = 0; i < attributeCount; i++) { - attributes.put(strings[getVarint()], strings[getVarint()]); - } - - Element e = createElement(name, attributes); - int childCount = getVarint(); - for (int i = 0; i < childCount; i++) { - e.addChild(readElement(strings)); - } - return e; - } - - private Element createElement(String name, Map attributes) { - switch (name) { - case "class": { - JfrClass type = new JfrClass(attributes); - if (!attributes.containsKey("superType")) { - types.put(type.id, type); - } - typesByName.put(type.name, type); - return type; - } - case "field": - return new JfrField(attributes); - default: - return new Element(); - } - } - - private void readConstantPool(long cpOffset) throws IOException { - long delta; - do { - seek(cpOffset); - ensureBytes(5); - - int posBeforeSize = buf.position(); - ensureBytes(getVarint() - (buf.position() - posBeforeSize)); - getVarint(); - getVarlong(); - getVarlong(); - delta = getVarlong(); - getVarint(); - - int poolCount = getVarint(); - for (int i = 0; i < poolCount; i++) { - int type = getVarint(); - readConstants(types.get(type)); - } - } while (delta != 0 && (cpOffset += delta) > 0); - } - - private void readConstants(JfrClass type) { - switch (type.name) { - case "jdk.types.ChunkHeader": - buf.position(buf.position() + (CHUNK_HEADER_SIZE + 3)); - break; - case "java.lang.Thread": - readThreads(type.fields.size()); - break; - case "java.lang.Class": - readClasses(type.fields.size()); - break; - case "java.lang.String": - readStrings(); - break; - case "jdk.types.Symbol": - readSymbols(); - break; - case "jdk.types.Method": - readMethods(); - break; - case "jdk.types.StackTrace": - readStackTraces(); - break; - default: - if (type.simpleType && type.fields.size() == 1) { - readEnumValues(type.name); - } else { - readOtherConstants(type.fields); - } - } - } - - private void readThreads(int fieldCount) { - int count = threads.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - long id = getVarlong(); - String osName = getString(); - int osThreadId = getVarint(); - String javaName = getString(); - long javaThreadId = getVarlong(); - readFields(fieldCount - 4); - javaThreads.put(id, javaThreadId); - threads.put(id, javaName != null ? javaName : osName); - } - } - - private void readClasses(int fieldCount) { - int count = classes.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - long id = getVarlong(); - long loader = getVarlong(); - long name = getVarlong(); - long pkg = getVarlong(); - int modifiers = getVarint(); - readFields(fieldCount - 4); - classes.put(id, new ClassRef(name)); - } - } - - private void readMethods() { - int count = methods.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - long id = getVarlong(); - long cls = getVarlong(); - long name = getVarlong(); - long sig = getVarlong(); - int modifiers = getVarint(); - int hidden = getVarint(); - methods.put(id, new MethodRef(cls, name, sig)); - } - } - - private void readStackTraces() { - int count = stackTraces.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - long id = getVarlong(); - int truncated = getVarint(); - StackTrace stackTrace = readStackTrace(); - stackTraces.put(id, stackTrace); - } - } - - private StackTrace readStackTrace() { - int depth = getVarint(); - long[] methods = new long[depth]; - byte[] types = new byte[depth]; - int[] locations = new int[depth]; - for (int i = 0; i < depth; i++) { - methods[i] = getVarlong(); - int line = getVarint(); - int bci = getVarint(); - locations[i] = line << 16 | (bci & 0xffff); - types[i] = buf.get(); - } - return new StackTrace(methods, types, locations); - } - - private void readStrings() { - int count = strings.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - strings.put(getVarlong(), getString()); - } - } - - private void readSymbols() { - int count = symbols.preallocate(getVarint()); - for (int i = 0; i < count; i++) { - long id = getVarlong(); - if (buf.get() != 3) { - throw new IllegalArgumentException("Invalid symbol encoding"); - } - symbols.put(id, getBytes()); - } - } - - private void readEnumValues(String typeName) { - HashMap map = new HashMap<>(); - int count = getVarint(); - for (int i = 0; i < count; i++) { - map.put((int) getVarlong(), getString()); - } - enums.put(typeName, map); - } - - private void readOtherConstants(List fields) { - int stringType = getTypeId("java.lang.String"); - - boolean[] numeric = new boolean[fields.size()]; - for (int i = 0; i < numeric.length; i++) { - JfrField f = fields.get(i); - numeric[i] = f.constantPool || f.type != stringType; - } - - int count = getVarint(); - for (int i = 0; i < count; i++) { - getVarlong(); - readFields(numeric); - } - } - - private void readFields(boolean[] numeric) { - for (boolean n : numeric) { - if (n) { - getVarlong(); - } else { - getString(); - } - } - } - - private void readFields(int count) { - while (count-- > 0) { - getVarlong(); - } - } - - private void cacheEventTypes() { - executionSample = getTypeId("jdk.ExecutionSample"); - nativeMethodSample = getTypeId("jdk.NativeMethodSample"); - wallClockSample = getTypeId("profiler.WallClockSample"); - allocationInNewTLAB = getTypeId("jdk.ObjectAllocationInNewTLAB"); - allocationOutsideTLAB = getTypeId("jdk.ObjectAllocationOutsideTLAB"); - allocationSample = getTypeId("jdk.ObjectAllocationSample"); - liveObject = getTypeId("profiler.LiveObject"); - monitorEnter = getTypeId("jdk.JavaMonitorEnter"); - threadPark = getTypeId("jdk.ThreadPark"); - activeSetting = getTypeId("jdk.ActiveSetting"); - malloc = getTypeId("profiler.Malloc"); - free = getTypeId("profiler.Free"); - - registerEvent("jdk.CPULoad", CPULoad.class); - registerEvent("jdk.GCHeapSummary", GCHeapSummary.class); - registerEvent("jdk.ObjectCount", ObjectCount.class); - registerEvent("jdk.ObjectCountAfterGC", ObjectCount.class); - } - - private int getTypeId(String typeName) { - JfrClass type = typesByName.get(typeName); - return type != null ? type.id : -1; - } - - public int getEnumKey(String typeName, String value) { - Map enumValues = enums.get(typeName); - if (enumValues != null) { - for (Map.Entry entry : enumValues.entrySet()) { - if (value.equals(entry.getValue())) { - return entry.getKey(); - } - } - } - return -1; - } - - public String getEnumValue(String typeName, int key) { - return enums.get(typeName).get(key); - } - - public int getVarint() { - int result = 0; - for (int shift = 0; ; shift += 7) { - byte b = buf.get(); - result |= (b & 0x7f) << shift; - if (b >= 0) { - return result; - } - } - } - - public long getVarlong() { - long result = 0; - for (int shift = 0; shift < 56; shift += 7) { - byte b = buf.get(); - result |= (b & 0x7fL) << shift; - if (b >= 0) { - return result; - } - } - return result | (buf.get() & 0xffL) << 56; - } - - public float getFloat() { - return buf.getFloat(); - } - - public double getDouble() { - return buf.getDouble(); - } - - public String getString() { - switch (buf.get()) { - case 0: - return null; - case 1: - return ""; - case 2: - return strings.get(getVarlong()); - case 3: - return new String(getBytes(), StandardCharsets.UTF_8); - case 4: { - char[] chars = new char[getVarint()]; - for (int i = 0; i < chars.length; i++) { - chars[i] = (char) getVarint(); - } - return new String(chars); - } - case 5: - return new String(getBytes(), StandardCharsets.ISO_8859_1); - default: - throw new IllegalArgumentException("Invalid string encoding"); - } - } - - public byte[] getBytes() { - byte[] bytes = new byte[getVarint()]; - buf.get(bytes); - return bytes; - } - - private void seek(long pos) throws IOException { - long bufPosition = pos - filePosition; - if (bufPosition >= 0 && bufPosition <= buf.limit()) { - buf.position((int) bufPosition); - } else { - filePosition = pos; - ch.position(pos); - buf.rewind().flip(); - } - } - - private boolean ensureBytes(int needed) throws IOException { - if (buf.remaining() >= needed) { - return true; - } - - if (ch == null) { - return false; - } - - filePosition += buf.position(); - - if (buf.capacity() < needed) { - ByteBuffer newBuf = ByteBuffer.allocateDirect(needed); - newBuf.put(buf); - buf = newBuf; + } + + if (type == executionSample || type == nativeMethodSample) { + if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(false); + } else if (type == wallClockSample) { + if (cls == null || cls == ExecutionSample.class) return (E) readExecutionSample(true); + } else if (type == allocationInNewTLAB) { + if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(true); + } else if (type == allocationOutsideTLAB || type == allocationSample) { + if (cls == null || cls == AllocationSample.class) return (E) readAllocationSample(false); + } else if (type == malloc) { + if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(true); + } else if (type == free) { + if (cls == null || cls == MallocEvent.class) return (E) readMallocEvent(false); + } else if (type == liveObject) { + if (cls == null || cls == LiveObject.class) return (E) readLiveObject(); + } else if (type == monitorEnter) { + if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(false); + } else if (type == threadPark) { + if (cls == null || cls == ContendedLock.class) return (E) readContendedLock(true); + } else if (type == activeSetting) { + readActiveSetting(); + } else { + Constructor customEvent = customEvents.get(type); + if (customEvent != null && (cls == null || cls == customEvent.getDeclaringClass())) { + try { + return (E) customEvent.newInstance(this); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } finally { + seek(filePosition + pos + size); + } + } + } + + seek(filePosition + pos + size); + } + + state = STATE_EOF; + return null; + } + + private ExecutionSample readExecutionSample(boolean hasSamples) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int threadState = getVarint(); + int samples = hasSamples ? getVarint() : 1; + return new ExecutionSample(time, tid, stackTraceId, threadState, samples); + } + + private AllocationSample readAllocationSample(boolean tlab) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + long allocationSize = getVarlong(); + long tlabSize = tlab ? getVarlong() : 0; + return new AllocationSample(time, tid, stackTraceId, classId, allocationSize, tlabSize); + } + + private MallocEvent readMallocEvent(boolean hasSize) { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + long address = getVarlong(); + long size = hasSize ? getVarlong() : 0; + return new MallocEvent(time, tid, stackTraceId, address, size); + } + + private LiveObject readLiveObject() { + long time = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + long allocationSize = getVarlong(); + long allocatimeTime = getVarlong(); + return new LiveObject(time, tid, stackTraceId, classId, allocationSize, allocatimeTime); + } + + private ContendedLock readContendedLock(boolean hasTimeout) { + long time = getVarlong(); + long duration = getVarlong(); + int tid = getVarint(); + int stackTraceId = getVarint(); + int classId = getVarint(); + if (hasTimeout) getVarlong(); + getVarlong(); + getVarlong(); + return new ContendedLock(time, tid, stackTraceId, duration, classId); + } + + private void readActiveSetting() { + JfrClass activeSetting = typesByName.get("jdk.ActiveSetting"); + if (activeSetting == null) return; + for (JfrField field : activeSetting.fields) { + getVarlong(); + if ("id".equals(field.name)) { + break; + } + } + String name = getString(); + String value = getString(); + settings.put(name, value); + } + + private boolean readChunk(int pos) throws IOException { + if (pos + CHUNK_HEADER_SIZE > buf.limit() || buf.getInt(pos) != CHUNK_SIGNATURE) { + throw new IOException("Not a valid JFR file"); + } + + int version = buf.getInt(pos + 4); + if (version < 0x20000 || version > 0x2ffff) { + throw new IOException( + "Unsupported JFR version: " + (version >>> 16) + "." + (version & 0xffff)); + } + + long chunkStart = filePosition + pos; + long chunkSize = buf.getLong(pos + 8); + if (chunkStart + chunkSize > fileSize) { + state = STATE_INCOMPLETE; + return false; + } + + long cpOffset = buf.getLong(pos + 16); + long metaOffset = buf.getLong(pos + 24); + if (cpOffset == 0 || metaOffset == 0) { + state = STATE_INCOMPLETE; + return false; + } + + chunkStartNanos = buf.getLong(pos + 32); + chunkEndNanos = buf.getLong(pos + 32) + buf.getLong(pos + 40); + chunkStartTicks = buf.getLong(pos + 48); + ticksPerSec = buf.getLong(pos + 56); + + startNanos = Math.min(startNanos, chunkStartNanos); + endNanos = Math.max(endNanos, chunkEndNanos); + startTicks = Math.min(startTicks, chunkStartTicks); + + types.clear(); + typesByName.clear(); + + readMeta(chunkStart + metaOffset); + readConstantPool(chunkStart + cpOffset); + cacheEventTypes(); + + seek(chunkStart + CHUNK_HEADER_SIZE); + state = STATE_READING; + return true; + } + + private void readMeta(long metaOffset) throws IOException { + seek(metaOffset); + ensureBytes(5); + + int posBeforeSize = buf.position(); + ensureBytes(getVarint() - (buf.position() - posBeforeSize)); + getVarint(); + getVarlong(); + getVarlong(); + getVarlong(); + + String[] strings = new String[getVarint()]; + for (int i = 0; i < strings.length; i++) { + strings[i] = getString(); + } + readElement(strings); + } + + private Element readElement(String[] strings) { + String name = strings[getVarint()]; + + int attributeCount = getVarint(); + Map attributes = new HashMap<>(attributeCount); + for (int i = 0; i < attributeCount; i++) { + attributes.put(strings[getVarint()], strings[getVarint()]); + } + + Element e = createElement(name, attributes); + int childCount = getVarint(); + for (int i = 0; i < childCount; i++) { + e.addChild(readElement(strings)); + } + return e; + } + + private Element createElement(String name, Map attributes) { + switch (name) { + case "class": + { + JfrClass type = new JfrClass(attributes); + if (!attributes.containsKey("superType")) { + types.put(type.id, type); + } + typesByName.put(type.name, type); + return type; + } + case "field": + return new JfrField(attributes); + default: + return new Element.NoOpElement(); + } + } + + private void readConstantPool(long cpOffset) throws IOException { + long delta; + do { + seek(cpOffset); + ensureBytes(5); + + int posBeforeSize = buf.position(); + ensureBytes(getVarint() - (buf.position() - posBeforeSize)); + getVarint(); + getVarlong(); + getVarlong(); + delta = getVarlong(); + getVarint(); + + int poolCount = getVarint(); + for (int i = 0; i < poolCount; i++) { + int type = getVarint(); + readConstants(types.get(type)); + } + } while (delta != 0 && (cpOffset += delta) > 0); + } + + private void readConstants(JfrClass type) { + String typeName = type.name; + if (typeName == null) { + readOtherConstants(type.fields); + return; + } + switch (typeName) { + case "jdk.types.ChunkHeader": + buf.position(buf.position() + (CHUNK_HEADER_SIZE + 3)); + break; + case "java.lang.Thread": + readThreads(type.fields.size()); + break; + case "java.lang.Class": + readClasses(type.fields.size()); + break; + case "java.lang.String": + readStrings(); + break; + case "jdk.types.Symbol": + readSymbols(); + break; + case "jdk.types.Method": + readMethods(); + break; + case "jdk.types.StackTrace": + readStackTraces(); + break; + default: + if (type.simpleType && type.fields.size() == 1) { + readEnumValues(typeName); } else { - buf.compact(); - } - - while (ch.read(buf) > 0 && buf.position() < needed) { - // keep reading - } - buf.flip(); - return buf.limit() > 0; - } + readOtherConstants(type.fields); + } + } + } + + private void readThreads(int fieldCount) { + int count = threads.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + String osName = getString(); + getVarint(); // osThreadId + String javaName = getString(); + long javaThreadId = getVarlong(); + readFields(fieldCount - 4); + javaThreads.put(id, javaThreadId); + String threadName = javaName != null ? javaName : (osName != null ? osName : "Thread-" + id); + threads.put(id, threadName); + } + } + + private void readClasses(int fieldCount) { + int count = classes.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + getVarlong(); + long name = getVarlong(); + getVarlong(); + getVarint(); + readFields(fieldCount - 4); + classes.put(id, new ClassRef(name)); + } + } + + private void readMethods() { + int count = methods.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + long cls = getVarlong(); + long name = getVarlong(); + long sig = getVarlong(); + getVarint(); + getVarint(); + methods.put(id, new MethodRef(cls, name, sig)); + } + } + + private void readStackTraces() { + int count = stackTraces.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + getVarint(); // int truncated + StackTrace stackTrace = readStackTrace(); + stackTraces.put(id, stackTrace); + } + } + + private StackTrace readStackTrace() { + int depth = getVarint(); + long[] methods = new long[depth]; + byte[] types = new byte[depth]; + int[] locations = new int[depth]; + for (int i = 0; i < depth; i++) { + methods[i] = getVarlong(); + int line = getVarint(); + int bci = getVarint(); + locations[i] = line << 16 | (bci & 0xffff); + types[i] = buf.get(); + } + return new StackTrace(methods, types, locations); + } + + private void readStrings() { + int count = strings.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + String str = getString(); + if (str == null) str = ""; + strings.put(getVarlong(), str); + } + } + + private void readSymbols() { + int count = symbols.preallocate(getVarint()); + for (int i = 0; i < count; i++) { + long id = getVarlong(); + if (buf.get() != 3) { + throw new IllegalArgumentException("Invalid symbol encoding"); + } + symbols.put(id, getBytes()); + } + } + + private void readEnumValues(@NotNull String typeName) { + HashMap map = new HashMap<>(); + int count = getVarint(); + for (int i = 0; i < count; i++) { + map.put((int) getVarlong(), getString()); + } + enums.put(typeName, map); + } + + private void readOtherConstants(List fields) { + int stringType = getTypeId("java.lang.String"); + + boolean[] numeric = new boolean[fields.size()]; + for (int i = 0; i < numeric.length; i++) { + JfrField f = fields.get(i); + numeric[i] = f.constantPool || f.type != stringType; + } + + int count = getVarint(); + for (int i = 0; i < count; i++) { + getVarlong(); + readFields(numeric); + } + } + + private void readFields(boolean[] numeric) { + for (boolean n : numeric) { + if (n) { + getVarlong(); + } else { + getString(); + } + } + } + + private void readFields(int count) { + while (count-- > 0) { + getVarlong(); + } + } + + private void cacheEventTypes() { + executionSample = getTypeId("jdk.ExecutionSample"); + nativeMethodSample = getTypeId("jdk.NativeMethodSample"); + wallClockSample = getTypeId("profiler.WallClockSample"); + allocationInNewTLAB = getTypeId("jdk.ObjectAllocationInNewTLAB"); + allocationOutsideTLAB = getTypeId("jdk.ObjectAllocationOutsideTLAB"); + allocationSample = getTypeId("jdk.ObjectAllocationSample"); + liveObject = getTypeId("profiler.LiveObject"); + monitorEnter = getTypeId("jdk.JavaMonitorEnter"); + threadPark = getTypeId("jdk.ThreadPark"); + activeSetting = getTypeId("jdk.ActiveSetting"); + malloc = getTypeId("profiler.Malloc"); + free = getTypeId("profiler.Free"); + + registerEvent("jdk.CPULoad", CPULoad.class); + registerEvent("jdk.GCHeapSummary", GCHeapSummary.class); + registerEvent("jdk.ObjectCount", ObjectCount.class); + registerEvent("jdk.ObjectCountAfterGC", ObjectCount.class); + } + + private int getTypeId(String typeName) { + JfrClass type = typesByName.get(typeName); + return type != null ? type.id : -1; + } + + public int getEnumKey(String typeName, String value) { + Map enumValues = enums.get(typeName); + if (enumValues != null) { + for (Map.Entry entry : enumValues.entrySet()) { + if (value.equals(entry.getValue())) { + return entry.getKey(); + } + } + } + return -1; + } + + public @Nullable String getEnumValue(String typeName, int key) { + Map enumMap = enums.get(typeName); + return enumMap != null ? enumMap.get(key) : null; + } + + public int getVarint() { + int result = 0; + for (int shift = 0; ; shift += 7) { + byte b = buf.get(); + result |= (b & 0x7f) << shift; + if (b >= 0) { + return result; + } + } + } + + public long getVarlong() { + long result = 0; + for (int shift = 0; shift < 56; shift += 7) { + byte b = buf.get(); + result |= (b & 0x7fL) << shift; + if (b >= 0) { + return result; + } + } + return result | (buf.get() & 0xffL) << 56; + } + + public float getFloat() { + return buf.getFloat(); + } + + public double getDouble() { + return buf.getDouble(); + } + + public @Nullable String getString() { + switch (buf.get()) { + case 0: + return null; + case 1: + return ""; + case 2: + return strings.get(getVarlong()); + case 3: + return new String(getBytes(), StandardCharsets.UTF_8); + case 4: + { + char[] chars = new char[getVarint()]; + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) getVarint(); + } + return new String(chars); + } + case 5: + return new String(getBytes(), StandardCharsets.ISO_8859_1); + default: + throw new IllegalArgumentException("Invalid string encoding"); + } + } + + public byte[] getBytes() { + byte[] bytes = new byte[getVarint()]; + buf.get(bytes); + return bytes; + } + + private void seek(long pos) throws IOException { + long bufPosition = pos - filePosition; + if (bufPosition >= 0 && bufPosition <= buf.limit()) { + buf.position((int) bufPosition); + } else { + filePosition = pos; + if (ch != null) { + ch.position(pos); + } + buf.rewind().flip(); + } + } + + private boolean ensureBytes(int needed) throws IOException { + if (buf.remaining() >= needed) { + return true; + } + + if (ch == null) { + return false; + } + + filePosition += buf.position(); + + if (buf.capacity() < needed) { + ByteBuffer newBuf = ByteBuffer.allocateDirect(needed); + newBuf.put(buf); + buf = newBuf; + } else { + buf.compact(); + } + + while (ch.read(buf) > 0 && buf.position() < needed) { + // keep reading + } + buf.flip(); + return buf.limit() > 0; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java index 79e967783df..4e4f203daf3 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java @@ -5,14 +5,14 @@ package io.sentry.protocol.jfr.jfr; -public class MethodRef { - public final long cls; - public final long name; - public final long sig; +public final class MethodRef { + public final long cls; + public final long name; + public final long sig; - public MethodRef(long cls, long name, long sig) { - this.cls = cls; - this.name = name; - this.sig = sig; - } + public MethodRef(long cls, long name, long sig) { + this.cls = cls; + this.name = name; + this.sig = sig; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java index 519ce407fb2..e3fda8c8a1b 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java @@ -5,14 +5,14 @@ package io.sentry.protocol.jfr.jfr; -public class StackTrace { - public final long[] methods; - public final byte[] types; - public final int[] locations; +public final class StackTrace { + public final long[] methods; + public final byte[] types; + public final int[] locations; - public StackTrace(long[] methods, byte[] types, int[] locations) { - this.methods = methods; - this.types = types; - this.locations = locations; - } + public StackTrace(long[] methods, byte[] types, int[] locations) { + this.methods = methods; + this.types = types; + this.locations = locations; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java index 5f0faef7eb6..c852d0f1b80 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java @@ -5,39 +5,40 @@ package io.sentry.protocol.jfr.jfr.event; -public class AllocationSample extends Event { - public final int classId; - public final long allocationSize; - public final long tlabSize; +public final class AllocationSample extends Event { + public final int classId; + public final long allocationSize; + public final long tlabSize; - public AllocationSample(long time, int tid, int stackTraceId, int classId, long allocationSize, long tlabSize) { - super(time, tid, stackTraceId); - this.classId = classId; - this.allocationSize = allocationSize; - this.tlabSize = tlabSize; - } + public AllocationSample( + long time, int tid, int stackTraceId, int classId, long allocationSize, long tlabSize) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.tlabSize = tlabSize; + } - @Override - public int hashCode() { - return classId * 127 + stackTraceId + (tlabSize == 0 ? 17 : 0); - } + @Override + public int hashCode() { + return classId * 127 + stackTraceId + (tlabSize == 0 ? 17 : 0); + } - @Override - public boolean sameGroup(Event o) { - if (o instanceof AllocationSample) { - AllocationSample a = (AllocationSample) o; - return classId == a.classId && (tlabSize == 0) == (a.tlabSize == 0); - } - return false; + @Override + public boolean sameGroup(Event o) { + if (o instanceof AllocationSample) { + AllocationSample a = (AllocationSample) o; + return classId == a.classId && (tlabSize == 0) == (a.tlabSize == 0); } + return false; + } - @Override - public long classId() { - return classId; - } + @Override + public long classId() { + return classId; + } - @Override - public long value() { - return tlabSize != 0 ? tlabSize : allocationSize; - } + @Override + public long value() { + return tlabSize != 0 ? tlabSize : allocationSize; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java index 6a955cf9e23..d504bf20739 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java @@ -7,15 +7,15 @@ import io.sentry.protocol.jfr.jfr.JfrReader; -public class CPULoad extends Event { - public final float jvmUser; - public final float jvmSystem; - public final float machineTotal; +public final class CPULoad extends Event { + public final float jvmUser; + public final float jvmSystem; + public final float machineTotal; - public CPULoad(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.jvmUser = jfr.getFloat(); - this.jvmSystem = jfr.getFloat(); - this.machineTotal = jfr.getFloat(); - } + public CPULoad(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.jvmUser = jfr.getFloat(); + this.jvmSystem = jfr.getFloat(); + this.machineTotal = jfr.getFloat(); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java index bc01e294b83..763edb5133b 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java @@ -5,37 +5,37 @@ package io.sentry.protocol.jfr.jfr.event; -public class ContendedLock extends Event { - public final long duration; - public final int classId; +public final class ContendedLock extends Event { + public final long duration; + public final int classId; - public ContendedLock(long time, int tid, int stackTraceId, long duration, int classId) { - super(time, tid, stackTraceId); - this.duration = duration; - this.classId = classId; - } + public ContendedLock(long time, int tid, int stackTraceId, long duration, int classId) { + super(time, tid, stackTraceId); + this.duration = duration; + this.classId = classId; + } - @Override - public int hashCode() { - return classId * 127 + stackTraceId; - } + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } - @Override - public boolean sameGroup(Event o) { - if (o instanceof ContendedLock) { - ContendedLock c = (ContendedLock) o; - return classId == c.classId; - } - return false; + @Override + public boolean sameGroup(Event o) { + if (o instanceof ContendedLock) { + ContendedLock c = (ContendedLock) o; + return classId == c.classId; } + return false; + } - @Override - public long classId() { - return classId; - } + @Override + public long classId() { + return classId; + } - @Override - public long value() { - return duration; - } + @Override + public long value() { + return duration; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java index 2493e3eb5fb..6cddf8bc48b 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java @@ -8,55 +8,59 @@ import java.lang.reflect.Field; public abstract class Event implements Comparable { - public final long time; - public final int tid; - public final int stackTraceId; - - protected Event(long time, int tid, int stackTraceId) { - this.time = time; - this.tid = tid; - this.stackTraceId = stackTraceId; - } + public final long time; + public final int tid; + public final int stackTraceId; - @Override - public int compareTo(Event o) { - return Long.compare(time, o.time); - } + protected Event(long time, int tid, int stackTraceId) { + this.time = time; + this.tid = tid; + this.stackTraceId = stackTraceId; + } - @Override - public int hashCode() { - return stackTraceId; - } + @Override + public int compareTo(Event o) { + return Long.compare(time, o.time); + } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(getClass().getSimpleName()) - .append("{time=").append(time) - .append(",tid=").append(tid) - .append(",stackTraceId=").append(stackTraceId); - for (Field f : getClass().getDeclaredFields()) { - try { - sb.append(',').append(f.getName()).append('=').append(f.get(this)); - } catch (ReflectiveOperationException e) { - break; - } - } - return sb.append('}').toString(); - } + @Override + public int hashCode() { + return stackTraceId; + } - public boolean sameGroup(Event o) { - return getClass() == o.getClass(); + @Override + public String toString() { + StringBuilder sb = + new StringBuilder(getClass().getSimpleName()) + .append("{time=") + .append(time) + .append(",tid=") + .append(tid) + .append(",stackTraceId=") + .append(stackTraceId); + for (Field f : getClass().getDeclaredFields()) { + try { + sb.append(',').append(f.getName()).append('=').append(f.get(this)); + } catch (ReflectiveOperationException e) { + break; + } } + return sb.append('}').toString(); + } - public long classId() { - return 0; - } + public boolean sameGroup(Event o) { + return getClass() == o.getClass(); + } - public long samples() { - return 1; - } + public long classId() { + return 0; + } - public long value() { - return 1; - } + public long samples() { + return 1; + } + + public long value() { + return 1; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java index 00bccf89203..56bf66ebd86 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java @@ -5,145 +5,151 @@ package io.sentry.protocol.jfr.jfr.event; -public class EventAggregator implements EventCollector { - private static final int INITIAL_CAPACITY = 1024; - - private final boolean threads; - private final double grain; - private Event[] keys; - private long[] samples; - private long[] values; - private int size; - private double fraction; - - public EventAggregator(boolean threads, double grain) { - this.threads = threads; - this.grain = grain; - - beforeChunk(); +import org.jetbrains.annotations.NotNull; + +public final class EventAggregator implements EventCollector { + private static final int INITIAL_CAPACITY = 1024; + + private final boolean threads; + private final double grain; + private @NotNull Event[] keys; + private @NotNull long[] samples; + private @NotNull long[] values; + private int size; + private double fraction; + + public EventAggregator(boolean threads, double grain) { + this.threads = threads; + this.grain = grain; + this.keys = new Event[INITIAL_CAPACITY]; + this.samples = new long[INITIAL_CAPACITY]; + this.values = new long[INITIAL_CAPACITY]; + + beforeChunk(); + } + + public int size() { + return size; + } + + @Override + public void collect(Event e) { + collect(e, e.samples(), e.value()); + } + + public void collect(Event e, long samples, long value) { + int mask = keys.length - 1; + int i = hashCode(e) & mask; + while (keys[i] != null) { + if (sameGroup(keys[i], e)) { + this.samples[i] += samples; + this.values[i] += value; + return; + } + i = (i + 1) & mask; } - public int size() { - return size; - } + this.keys[i] = e; + this.samples[i] = samples; + this.values[i] = value; - @Override - public void collect(Event e) { - collect(e, e.samples(), e.value()); + if (++size * 2 > keys.length) { + resize(keys.length * 2); } - - public void collect(Event e, long samples, long value) { - int mask = keys.length - 1; - int i = hashCode(e) & mask; - while (keys[i] != null) { - if (sameGroup(keys[i], e)) { - this.samples[i] += samples; - this.values[i] += value; - return; - } - i = (i + 1) & mask; - } - - this.keys[i] = e; - this.samples[i] = samples; - this.values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } + } + + @Override + public void beforeChunk() { + if (keys == null || size > 0) { + keys = new Event[INITIAL_CAPACITY]; + samples = new long[INITIAL_CAPACITY]; + values = new long[INITIAL_CAPACITY]; + size = 0; } + } - @Override - public void beforeChunk() { - if (keys == null || size > 0) { - keys = new Event[INITIAL_CAPACITY]; - samples = new long[INITIAL_CAPACITY]; - values = new long[INITIAL_CAPACITY]; - size = 0; - } + @Override + public void afterChunk() { + if (grain > 0) { + coarsen(grain); } - - @Override - public void afterChunk() { - if (grain > 0) { - coarsen(grain); + } + + @Override + public boolean finish() { + // Don't set to null as it would break nullability contract + keys = new Event[0]; + samples = new long[0]; + values = new long[0]; + return false; + } + + @Override + public void forEach(Visitor visitor) { + if (size > 0) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + visitor.visit(keys[i], samples[i], values[i]); } + } } - - @Override - public boolean finish() { - keys = null; - samples = null; - values = null; - return false; - } - - @Override - public void forEach(Visitor visitor) { - if (size > 0) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - visitor.visit(keys[i], samples[i], values[i]); - } - } + } + + public void coarsen(double grain) { + fraction = 0; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + long s0 = samples[i]; + long s1 = round(s0 / grain); + if (s1 == 0) { + keys[i] = null; + size--; } + samples[i] = s1; + values[i] = (long) (values[i] * ((double) s1 / s0)); + } } + } - public void coarsen(double grain) { - fraction = 0; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - long s0 = samples[i]; - long s1 = round(s0 / grain); - if (s1 == 0) { - keys[i] = null; - size--; - } - samples[i] = s1; - values[i] = (long) (values[i] * ((double) s1 / s0)); - } - } + private long round(double d) { + long r = (long) d; + if ((fraction += d - r) >= 1.0) { + fraction -= 1.0; + r++; } - - private long round(double d) { - long r = (long) d; - if ((fraction += d - r) >= 1.0) { - fraction -= 1.0; - r++; + return r; + } + + private int hashCode(Event e) { + return e.hashCode() + (threads ? e.tid * 31 : 0); + } + + private boolean sameGroup(Event e1, Event e2) { + return e1.stackTraceId == e2.stackTraceId && (!threads || e1.tid == e2.tid) && e1.sameGroup(e2); + } + + private void resize(int newCapacity) { + Event[] newKeys = new Event[newCapacity]; + long[] newSamples = new long[newCapacity]; + long[] newValues = new long[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == null) { + newKeys[j] = keys[i]; + newSamples[j] = samples[i]; + newValues[j] = values[i]; + break; + } } - return r; - } - - private int hashCode(Event e) { - return e.hashCode() + (threads ? e.tid * 31 : 0); + } } - private boolean sameGroup(Event e1, Event e2) { - return e1.stackTraceId == e2.stackTraceId && (!threads || e1.tid == e2.tid) && e1.sameGroup(e2); - } - - private void resize(int newCapacity) { - Event[] newKeys = new Event[newCapacity]; - long[] newSamples = new long[newCapacity]; - long[] newValues = new long[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == null) { - newKeys[j] = keys[i]; - newSamples[j] = samples[i]; - newValues[j] = values[i]; - break; - } - } - } - } - - keys = newKeys; - samples = newSamples; - values = newValues; - } + keys = newKeys; + samples = newSamples; + values = newValues; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java index b35fc0a2c71..4ae81889f6b 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java @@ -7,18 +7,18 @@ public interface EventCollector { - void collect(Event e); + void collect(Event e); - void beforeChunk(); + void beforeChunk(); - void afterChunk(); + void afterChunk(); - // Returns true if this collector has remaining data to process - boolean finish(); + // Returns true if this collector has remaining data to process + boolean finish(); - void forEach(Visitor visitor); + void forEach(Visitor visitor); - interface Visitor { - void visit(Event event, long samples, long value); - } + interface Visitor { + void visit(Event event, long samples, long value); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java index 8b8b2cbb3cb..3bf836c7a7e 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java @@ -5,23 +5,23 @@ package io.sentry.protocol.jfr.jfr.event; -public class ExecutionSample extends Event { - public final int threadState; - public final int samples; +public final class ExecutionSample extends Event { + public final int threadState; + public final int samples; - public ExecutionSample(long time, int tid, int stackTraceId, int threadState, int samples) { - super(time, tid, stackTraceId); - this.threadState = threadState; - this.samples = samples; - } + public ExecutionSample(long time, int tid, int stackTraceId, int threadState, int samples) { + super(time, tid, stackTraceId); + this.threadState = threadState; + this.samples = samples; + } - @Override - public long samples() { - return samples; - } + @Override + public long samples() { + return samples; + } - @Override - public long value() { - return samples; - } + @Override + public long value() { + return samples; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java index 6f4ca0b7466..ae72cadf3d5 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java @@ -7,22 +7,22 @@ import io.sentry.protocol.jfr.jfr.JfrReader; -public class GCHeapSummary extends Event { - public final int gcId; - public final boolean afterGC; - public final long committed; - public final long reserved; - public final long used; +public final class GCHeapSummary extends Event { + public final int gcId; + public final boolean afterGC; + public final long committed; + public final long reserved; + public final long used; - public GCHeapSummary(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.gcId = jfr.getVarint(); - this.afterGC = jfr.getVarint() > 0; - long start = jfr.getVarlong(); - long committedEnd = jfr.getVarlong(); - this.committed = jfr.getVarlong(); - long reservedEnd = jfr.getVarlong(); - this.reserved = jfr.getVarlong(); - this.used = jfr.getVarlong(); - } + public GCHeapSummary(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.afterGC = jfr.getVarint() > 0; + jfr.getVarlong(); // long start + jfr.getVarlong(); // long committedEnd + this.committed = jfr.getVarlong(); + jfr.getVarlong(); // long reservedEnd + this.reserved = jfr.getVarlong(); + this.used = jfr.getVarlong(); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java index a7f7d60cb79..ba33391559d 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java @@ -5,39 +5,40 @@ package io.sentry.protocol.jfr.jfr.event; -public class LiveObject extends Event { - public final int classId; - public final long allocationSize; - public final long allocationTime; +public final class LiveObject extends Event { + public final int classId; + public final long allocationSize; + public final long allocationTime; - public LiveObject(long time, int tid, int stackTraceId, int classId, long allocationSize, long allocationTime) { - super(time, tid, stackTraceId); - this.classId = classId; - this.allocationSize = allocationSize; - this.allocationTime = allocationTime; - } + public LiveObject( + long time, int tid, int stackTraceId, int classId, long allocationSize, long allocationTime) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.allocationTime = allocationTime; + } - @Override - public int hashCode() { - return classId * 127 + stackTraceId; - } + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } - @Override - public boolean sameGroup(Event o) { - if (o instanceof LiveObject) { - LiveObject a = (LiveObject) o; - return classId == a.classId; - } - return false; + @Override + public boolean sameGroup(Event o) { + if (o instanceof LiveObject) { + LiveObject a = (LiveObject) o; + return classId == a.classId; } + return false; + } - @Override - public long classId() { - return classId; - } + @Override + public long classId() { + return classId; + } - @Override - public long value() { - return allocationSize; - } + @Override + public long value() { + return allocationSize; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java index 07249391540..a67d2f6fc72 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java @@ -5,18 +5,18 @@ package io.sentry.protocol.jfr.jfr.event; -public class MallocEvent extends Event { - public final long address; - public final long size; +public final class MallocEvent extends Event { + public final long address; + public final long size; - public MallocEvent(long time, int tid, int stackTraceId, long address, long size) { - super(time, tid, stackTraceId); - this.address = address; - this.size = size; - } + public MallocEvent(long time, int tid, int stackTraceId, long address, long size) { + super(time, tid, stackTraceId); + this.address = address; + this.size = size; + } - @Override - public long value() { - return size; - } + @Override + public long value() { + return size; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java index 31c57467c33..556fa8b9795 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java @@ -5,61 +5,63 @@ package io.sentry.protocol.jfr.jfr.event; -import java.util.List; import java.util.ArrayList; -import java.util.Map; import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; -public class MallocLeakAggregator implements EventCollector { - private final EventCollector wrapped; - private final Map addresses; - private List events; - - public MallocLeakAggregator(EventCollector wrapped) { - this.wrapped = wrapped; - this.addresses = new HashMap<>(); - } +public final class MallocLeakAggregator implements EventCollector { + private final EventCollector wrapped; + private final Map addresses; + private @NotNull List events; - @Override - public void collect(Event e) { - events.add((MallocEvent) e); - } + public MallocLeakAggregator(@NotNull EventCollector wrapped) { + this.wrapped = wrapped; + this.addresses = new HashMap<>(); + this.events = new ArrayList<>(); + } - @Override - public void beforeChunk() { - events = new ArrayList<>(); - } + @Override + public void collect(Event e) { + events.add((MallocEvent) e); + } - @Override - public void afterChunk() { - events.sort(null); + @Override + public void beforeChunk() { + events = new ArrayList<>(); + } - for (MallocEvent e : events) { - if (e.size > 0) { - addresses.put(e.address, e); - } else { - addresses.remove(e.address); - } - } + @Override + public void afterChunk() { + events.sort(null); - events = null; + for (MallocEvent e : events) { + if (e.size > 0) { + addresses.put(e.address, e); + } else { + addresses.remove(e.address); + } } - @Override - public boolean finish() { - wrapped.beforeChunk(); - for (Event e : addresses.values()) { - wrapped.collect(e); - } - wrapped.afterChunk(); + events = new ArrayList<>(); + } - // Free memory before the final conversion - addresses.clear(); - return true; + @Override + public boolean finish() { + wrapped.beforeChunk(); + for (Event e : addresses.values()) { + wrapped.collect(e); } + wrapped.afterChunk(); - @Override - public void forEach(Visitor visitor) { - wrapped.forEach(visitor); - } + // Free memory before the final conversion + addresses.clear(); + return true; + } + + @Override + public void forEach(Visitor visitor) { + wrapped.forEach(visitor); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java index fc0329558fd..a38df40372c 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java @@ -7,17 +7,17 @@ import io.sentry.protocol.jfr.jfr.JfrReader; -public class ObjectCount extends Event { - public final int gcId; - public final int classId; - public final long count; - public final long totalSize; +public final class ObjectCount extends Event { + public final int gcId; + public final int classId; + public final long count; + public final long totalSize; - public ObjectCount(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.gcId = jfr.getVarint(); - this.classId = jfr.getVarint(); - this.count = jfr.getVarlong(); - this.totalSize = jfr.getVarlong(); - } + public ObjectCount(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.classId = jfr.getVarint(); + this.count = jfr.getVarlong(); + this.totalSize = jfr.getVarlong(); + } } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java index 1a34a7de461..c9a49690247 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -1,5 +1,8 @@ package io.sentry.protocol.profiling; +import static io.sentry.DataCategory.All; +import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; + import io.sentry.DataCategory; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; @@ -20,12 +23,6 @@ import io.sentry.transport.RateLimiter; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.SentryRandom; -import one.profiler.AsyncProfiler; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.VisibleForTesting; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -34,13 +31,15 @@ import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; - -import static io.sentry.DataCategory.All; -import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; +import one.profiler.AsyncProfiler; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; @ApiStatus.Internal public final class JavaContinuousProfiler - implements IContinuousProfiler, RateLimiter.IRateLimitObserver { + implements IContinuousProfiler, RateLimiter.IRateLimitObserver { private static final long MAX_CHUNK_DURATION_MILLIS = 10000; private final @NotNull ILogger logger; @@ -69,10 +68,10 @@ public final class JavaContinuousProfiler private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock(); public JavaContinuousProfiler( - final @NotNull ILogger logger, - final @Nullable String profilingTracesDirPath, - final int profilingTracesHz, - final @NotNull ISentryExecutorService executorService) { + final @NotNull ILogger logger, + final @Nullable String profilingTracesDirPath, + final int profilingTracesHz, + final @NotNull ISentryExecutorService executorService) { this.logger = logger; this.profilingTracesDirPath = profilingTracesDirPath; this.profilingTracesHz = profilingTracesHz; @@ -88,37 +87,37 @@ private void init() { isInitialized = true; if (profilingTracesDirPath == null) { logger.log( - SentryLevel.WARNING, - "Disabling profiling because no profiling traces dir path is defined in options."); + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); return; } if (profilingTracesHz <= 0) { logger.log( - SentryLevel.WARNING, - "Disabling profiling because trace rate is set to %d", - profilingTracesHz); + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + profilingTracesHz); return; } -// profiler = -// new AndroidProfiler( -// profilingTracesDirPath, -// (int) SECONDS.toMicros(1) / profilingTracesHz, -// frameMetricsCollector, -// null, -// logger); + // profiler = + // new AndroidProfiler( + // profilingTracesDirPath, + // (int) SECONDS.toMicros(1) / profilingTracesHz, + // frameMetricsCollector, + // null, + // logger); } @SuppressWarnings("ReferenceEquality") @Override public void startProfiler( - final @NotNull ProfileLifecycle profileLifecycle, - final @NotNull TracesSampler tracesSampler) { + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (shouldSample) { isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); - //Kepp TRUE for now -// shouldSample = false; + // Kepp TRUE for now + // shouldSample = false; } if (!isSampled) { logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); @@ -135,7 +134,7 @@ public void startProfiler( @SuppressWarnings("ReferenceEquality") private void start() { if ((scopes == null || scopes == NoOpScopes.getInstance()) - && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { + && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.forkedRootScopes("profiler"); final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); if (rateLimiter != null) { @@ -153,8 +152,8 @@ private void start() { if (scopes != null) { final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); if (rateLimiter != null - && (rateLimiter.isActiveForCategory(All) - || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { + && (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); // Let's stop and reset profiler id, as the profile is now broken anyway stop(false); @@ -175,7 +174,7 @@ private void start() { filename = SentryUUID.generateSentryId() + ".jfr"; final String startData; try { -// System.out.println("### Starting profiler with start,jfr,event=wall,file"); + // System.out.println("### Starting profiler with start,jfr,event=wall,file"); startData = profiler.execute("start,jfr,event=cpu,alloc,file=" + filename); } catch (IOException e) { throw new RuntimeException(e); @@ -199,9 +198,9 @@ private void start() { stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } catch (RejectedExecutionException e) { logger.log( - SentryLevel.ERROR, - "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", - e); + SentryLevel.ERROR, + "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", + e); shouldStop = true; } } @@ -237,20 +236,20 @@ private void stop(final boolean restartProfiler) { // check if profiler end successfully if (endData == null) { logger.log( - SentryLevel.ERROR, - "An error occurred while collecting a profile chunk, and it won't be sent."); + SentryLevel.ERROR, + "An error occurred while collecting a profile chunk, and it won't be sent."); } else { // The scopes can be null if the profiler is started before the SDK is initialized (app // 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()) { payloadBuilders.add( - new ProfileChunk.Builder( - profilerId, - chunkId, - new HashMap<>(), - new File(filename), - startProfileChunkTimestamp)); + new ProfileChunk.Builder( + profilerId, + chunkId, + new HashMap<>(), + new File(filename), + startProfileChunkTimestamp)); } } @@ -300,24 +299,24 @@ public void close(final boolean isTerminating) { private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { try { options - .getExecutorService() - .submit( - () -> { - // SDK is closed, we don't send the chunks - if (isClosed.get()) { - return; - } - final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); - try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) { - for (ProfileChunk.Builder builder : payloadBuilders) { - payloads.add(builder.build(options)); - } - payloadBuilders.clear(); - } - for (ProfileChunk payload : payloads) { - scopes.captureProfileChunk(payload); - } - }); + .getExecutorService() + .submit( + () -> { + // SDK is closed, we don't send the chunks + if (isClosed.get()) { + return; + } + final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); + try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) { + for (ProfileChunk.Builder builder : payloadBuilders) { + payloads.add(builder.build(options)); + } + payloadBuilders.clear(); + } + for (ProfileChunk payload : payloads) { + scopes.captureProfileChunk(payload); + } + }); } catch (Throwable e) { options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e); } @@ -342,13 +341,12 @@ public int getRootSpanCounter() { @Override public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { // We stop the profiler as soon as we are rate limited, to avoid the performance overhead -// if (rateLimiter.isActiveForCategory(All) -// || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { -// logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); -// stop(false); -// } + // if (rateLimiter.isActiveForCategory(All) + // || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { + // logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + // stop(false); + // } // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's // useless to restart it automatically } } - diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java index f23e42848fe..e013ec594e6 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java @@ -1,30 +1,28 @@ package io.sentry.protocol.profiling; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.util.Map; - 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") + // @JsonProperty("function") public @Nullable String function; // e.g., "com.example.MyClass.myMethod" -// @JsonProperty("module") + // @JsonProperty("module") public @Nullable String module; // e.g., "com.example" (package name) -// @JsonProperty("filename") + // @JsonProperty("filename") public @Nullable String filename; // e.g., "MyClass.java" -// @JsonProperty("lineno") + // @JsonProperty("lineno") public @Nullable Integer lineno; // Line number (nullable) -// @JsonProperty("abs_path") + // @JsonProperty("abs_path") public @Nullable String absPath; // Optional: Absolute path if available public static final class JsonKeys { @@ -39,18 +37,18 @@ public static final class JsonKeys { public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); - if(function != null) { + if (function != null) { writer.name(JsonKeys.FUNCTION).value(logger, function); } - if(module != null) { + if (module != null) { writer.name(JsonKeys.MODULE).value(logger, module); } - if(filename != null) { + if (filename != null) { writer.name(JsonKeys.FILENAME).value(logger, filename); } - if(lineno != null) { + if (lineno != null) { writer.name(JsonKeys.LINE_NO).value(logger, lineno); } @@ -63,9 +61,7 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr } @Override - public void setUnknown(@Nullable Map unknown) { - - } + 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. diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java index 271034de024..d8aff8047db 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java @@ -1,12 +1,4 @@ package io.sentry.protocol.profiling; -import io.sentry.profilemeasurements.ProfileMeasurement; -import io.sentry.protocol.DebugMeta; -import io.sentry.protocol.SdkVersion; -import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryStackFrame; -import io.sentry.vendor.gson.stream.JsonToken; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import io.sentry.ILogger; import io.sentry.JsonDeserializer; @@ -14,13 +6,12 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; -import io.sentry.ProfileChunk; - +import io.sentry.protocol.SentryStackFrame; import java.io.IOException; -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 JfrProfile implements JsonUnknown, JsonSerializable { public @Nullable List samples; @@ -48,12 +39,12 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr 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.beginObject(); + // for (String key : threadMetadata.keySet()) { + // ThreadMetadata value = threadMetadata.get(key); + // writer.name(key).value(logger, value); + // } + // writer.endObject(); } if (unknown != null) { @@ -85,47 +76,49 @@ public static final class JsonKeys { public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull JfrProfile deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + public @NotNull JfrProfile deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { reader.beginObject(); JfrProfile data = new JfrProfile(); 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; + // 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; } } - } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java index 65b89418ec0..73f7909ae04 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java @@ -1,18 +1,16 @@ package io.sentry.protocol.profiling; +import io.sentry.ILogger; import io.sentry.JsonDeserializer; +import io.sentry.JsonSerializable; +import io.sentry.JsonUnknown; import io.sentry.ObjectReader; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - +import io.sentry.ObjectWriter; import java.io.IOException; import java.util.HashMap; import java.util.Map; - -import io.sentry.ILogger; -import io.sentry.JsonSerializable; -import io.sentry.JsonUnknown; -import io.sentry.ObjectWriter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class JfrSample implements JsonUnknown, JsonSerializable { @@ -34,7 +32,7 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); writer.name(JsonKeys.STACK_ID).value(logger, stackId); - if(threadId != null) { + if (threadId != null) { writer.name(JsonKeys.THREAD_ID).value(logger, threadId); } @@ -47,14 +45,13 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr } @Override - public void setUnknown(@Nullable Map unknown) { - - } + public void setUnknown(@Nullable Map unknown) {} public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull JfrSample deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + public @NotNull JfrSample deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { reader.beginObject(); JfrSample data = new JfrSample(); return data; diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java index e2946f2939b..7c049ce086f 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java @@ -1,43 +1,43 @@ -//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 { +// 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 { @@ -70,7 +70,8 @@ // 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; +// 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"; @@ -164,12 +165,14 @@ // // if (thread != null) { // long osId = thread.getOSThreadId(); -// String name = thread.getJavaName() != null ? thread.getJavaName() : thread.getOSName(); +// 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(); +// String name = eventThread.getJavaName() != null ? eventThread.getJavaName() : +// eventThread.getOSName(); // if (osId > 0 && name != null) threadNamesByOSId.put(osId, name); // } // try { @@ -221,24 +224,29 @@ // try { // if (event.hasField("sampledThread")) { // RecordedThread eventThreadRef = event.getValue("sampledThread"); -// threadName = eventThreadRef.getJavaName() != null ? eventThreadRef.getJavaName() : eventThreadRef.getOSName(); +// 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 && 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."); +//// 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()); +// 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."); +// System.err.println("WARN: Invalid OS Thread ID (<= 0) for sample at " + timestamp + ". +// Skipping."); // continue; // } // String threadIdStr = String.valueOf(osThreadId); @@ -247,7 +255,8 @@ // // --- Thread Metadata --- // threadMetadata.computeIfAbsent(threadIdStr, tid -> { // ThreadMetadata meta = new ThreadMetadata(); -// meta.name = intermediateThreadName;//threadNamesByOSId.getOrDefault(intermediateThreadId, "Thread " + tid); +// meta.name = +// intermediateThreadName;//threadNamesByOSId.getOrDefault(intermediateThreadId, "Thread " + tid); // // meta.priority = ...; // Priority logic if needed // return meta; // }); @@ -344,4 +353,4 @@ // System.exit(1); // } // } -//} +// } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java index 7072807d932..9c83a686114 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/ThreadMetadata.java @@ -6,12 +6,11 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - 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" @@ -23,7 +22,6 @@ public static final class JsonKeys { public static final String PRIORITY = "priority"; } - @Override public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); @@ -40,18 +38,16 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr } @Override - public void setUnknown(@Nullable Map unknown) { - - } + public void setUnknown(@Nullable Map unknown) {} public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull ThreadMetadata deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + public @NotNull ThreadMetadata deserialize( + @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { reader.beginObject(); ThreadMetadata data = new ThreadMetadata(); return data; } } } - From f44e7fd9fed18abb02bb2fad30e6b3dafaae4b6e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 24 Jun 2025 11:01:52 +0200 Subject: [PATCH 05/31] add profile-session-sample-rate to external options --- sentry/src/main/java/io/sentry/ExternalOptions.java | 12 ++++++++++++ sentry/src/main/java/io/sentry/SentryOptions.java | 8 ++++---- .../src/test/java/io/sentry/ExternalOptionsTest.kt | 7 +++++++ sentry/src/test/java/io/sentry/SentryOptionsTest.kt | 2 ++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 3e40d05543d..4edeb975846 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -56,6 +56,8 @@ public final class ExternalOptions { private @Nullable Boolean forceInit; private @Nullable Boolean captureOpenTelemetryEvents; + private @Nullable Double profileSessionSampleRate; + private @Nullable SentryOptions.Cron cron; @SuppressWarnings("unchecked") @@ -202,6 +204,8 @@ public final class ExternalOptions { options.setEnableSpotlight(propertiesProvider.getBooleanProperty("enable-spotlight")); options.setSpotlightConnectionUrl(propertiesProvider.getProperty("spotlight-connection-url")); + options.setProfileSessionSampleRate( + propertiesProvider.getDoubleProperty("profile-session-sample-rate")); return options; } @@ -531,4 +535,12 @@ public void setEnableLogs(final @Nullable Boolean enableLogs) { public @Nullable Boolean isEnableLogs() { return enableLogs; } + + public @Nullable Double getProfileSessionSampleRate() { + return profileSessionSampleRate; + } + + public void setProfileSessionSampleRate(@Nullable Double profileSessionSampleRate) { + this.profileSessionSampleRate = profileSessionSampleRate; + } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 547811fc5e3..fb2f8dcface 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3048,10 +3048,6 @@ private SentryOptions(final boolean empty) { setSentryClientName(BuildConfig.SENTRY_JAVA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(sdkVersion); addPackageInfo(); - // TODO: make this configurable - // setProfileSessionSampleRate(1.0); - // setContinuousProfiler( - // new JavaContinuousProfiler(new SystemOutLogger(), "", 10, executorService)); } } @@ -3211,6 +3207,10 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isEnableLogs() != null) { getLogs().setEnabled(options.isEnableLogs()); } + + if (options.getProfileSessionSampleRate() != null) { + setProfileSessionSampleRate(options.getProfileSessionSampleRate()); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 8ff1d71d8eb..47153f59f8f 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -382,6 +382,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with profileSessionSampleRate set to 0_8`() { + withPropertiesFile("profile-session-sample-rate=0.8") { options -> + assertTrue(options.profileSessionSampleRate == 0.8) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 91d4cb7f00e..15cd48162ad 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -415,6 +415,7 @@ class SentryOptionsTest { externalOptions.spotlightConnectionUrl = "http://local.sentry.io:1234" externalOptions.isGlobalHubMode = true externalOptions.isEnableLogs = true + externalOptions.profileSessionSampleRate = 0.8 val options = SentryOptions() @@ -460,6 +461,7 @@ class SentryOptionsTest { assertEquals("http://local.sentry.io:1234", options.spotlightConnectionUrl) assertTrue(options.isGlobalHubMode!!) assertTrue(options.logs.isEnabled!!) + assertEquals(0.8, options.profileSessionSampleRate) } @Test From 8b4c71ab50c8bdca818efc59493646db7bd11178 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 24 Jun 2025 12:42:48 +0200 Subject: [PATCH 06/31] add platform as constructor param to ProfileChunk, wip: set java continuous profiler in sentry init --- .../core/AndroidContinuousProfiler.java | 3 +- .../src/main/java/io/sentry/ProfileChunk.java | 30 +++++-------------- sentry/src/main/java/io/sentry/Sentry.java | 14 +++++++++ .../profiling/JavaContinuousProfiler.java | 3 +- .../test/java/io/sentry/JsonSerializerTest.kt | 2 +- .../test/java/io/sentry/SentryClientTest.kt | 2 +- 6 files changed, 28 insertions(+), 26 deletions(-) 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 e255e24eaa3..761f2936a1a 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 @@ -295,7 +295,8 @@ private void stop(final boolean restartProfiler) { chunkId, endData.measurementsMap, endData.traceFile, - startProfileChunkTimestamp)); + startProfileChunkTimestamp, + "android")); } } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index d9a88cc1e62..f7f7757b02f 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -45,29 +45,10 @@ public ProfileChunk() { new File("dummy"), new HashMap<>(), 0.0, + "android", SentryOptions.empty()); } - public ProfileChunk( - final @NotNull SentryId profilerId, - final @NotNull SentryId chunkId, - final @NotNull File traceFile, - final @NotNull Map measurements, - final @NotNull Double timestamp, - final @NotNull SentryOptions options) { - this.profilerId = profilerId; - this.chunkId = chunkId; - this.traceFile = traceFile; - this.measurements = measurements; - this.debugMeta = null; - this.clientSdk = options.getSdkVersion(); - this.release = options.getRelease() != null ? options.getRelease() : ""; - this.environment = options.getEnvironment(); - this.platform = "android"; - this.version = "2"; - this.timestamp = timestamp; - } - public ProfileChunk( final @NotNull SentryId profilerId, final @NotNull SentryId chunkId, @@ -196,21 +177,26 @@ public static final class Builder { private final @NotNull File traceFile; private final double timestamp; + private final @NotNull String platform; + public Builder( final @NotNull SentryId profilerId, final @NotNull SentryId chunkId, final @NotNull Map measurements, final @NotNull File traceFile, - final @NotNull SentryDate timestamp) { + final @NotNull SentryDate timestamp, + final @NotNull String platform) { this.profilerId = profilerId; this.chunkId = chunkId; this.measurements = new ConcurrentHashMap<>(measurements); this.traceFile = traceFile; this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); + this.platform = platform; } public ProfileChunk build(SentryOptions options) { - return new ProfileChunk(profilerId, chunkId, traceFile, measurements, timestamp, options); + return new ProfileChunk( + profilerId, chunkId, traceFile, measurements, timestamp, platform, options); } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 5345178f3ec..709e38782d7 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -18,6 +18,7 @@ import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; +import io.sentry.protocol.profiling.JavaContinuousProfiler; import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.DebugMetaPropertiesApplier; @@ -650,6 +651,19 @@ private static void initConfigurations(final @NotNull SentryOptions options) { } options.getBackpressureMonitor().start(); } + + // TODO: make this configurable + if (options.isContinuousProfilingEnabled()) { + options.setContinuousProfiler( + new JavaContinuousProfiler(new SystemOutLogger(), "", 10, options.getExecutorService())); + } + + options + .getLogger() + .log( + SentryLevel.INFO, + "Continuous profiler is enabled %s", + options.isContinuousProfilingEnabled()); } /** Close the SDK */ diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java index c9a49690247..6531d67e93e 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -249,7 +249,8 @@ private void stop(final boolean restartProfiler) { chunkId, new HashMap<>(), new File(filename), - startProfileChunkTimestamp)); + startProfileChunkTimestamp, + "java")); } } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 1ec53f14eba..512d23d1b1e 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -887,7 +887,7 @@ class JsonSerializerTest { fixture.options.sdkVersion = SdkVersion("test", "1.2.3") fixture.options.release = "release" fixture.options.environment = "environment" - val profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), 5.3, fixture.options) + val profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), 5.3, "android", fixture.options) val measurementNow = SentryNanotimeDate() val measurementNowSeconds = BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN) diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 4309f06f831..bffb37b9593 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -102,7 +102,7 @@ class SentryClientTest { whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), scopes) sentryTracer.startChild("a-span", "span 1").finish() - profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), 1.0, sentryOptions) + profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), 1.0, "android", sentryOptions) } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) From a44287b3d3c7f618a1792607d7bdf6e6b7ea7135 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 1 Jul 2025 16:12:20 +0200 Subject: [PATCH 07/31] add doubleToBigDecimal in JfrSample and ProfileChunk, same as we do it in SentrySpan to work around scientific notation of double, use wall clock profiling --- .../src/main/resources/application.properties | 1 + sentry/api/sentry.api | 5 +++-- sentry/src/main/java/io/sentry/ProfileChunk.java | 8 +++++++- .../protocol/profiling/JavaContinuousProfiler.java | 11 +---------- .../java/io/sentry/protocol/profiling/JfrSample.java | 9 ++++++++- 5 files changed, 20 insertions(+), 14 deletions(-) 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 faeebe7b314..3fb2f721186 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 @@ -17,6 +17,7 @@ sentry.enable-spotlight=false sentry.enablePrettySerializationOutput=false in-app-includes="io.sentry.samples" sentry.logs.enabled=true +sentry.profile-session-sample-rate=1.0 # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 467337416cf..eddc3a56fbb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -494,6 +494,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 getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProguardUuid ()Ljava/lang/String; public fun getProxy ()Lio/sentry/SentryOptions$Proxy; @@ -535,6 +536,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 setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V public fun setProguardUuid (Ljava/lang/String;)V public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V @@ -1938,7 +1940,6 @@ 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;Lio/sentry/SentryOptions;)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 equals (Ljava/lang/Object;)Z public fun getChunkId ()Lio/sentry/protocol/SentryId; @@ -1964,7 +1965,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;)V + 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 build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index f7f7757b02f..cd7c34619a9 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -8,6 +8,8 @@ import io.sentry.vendor.gson.stream.JsonToken; import java.io.File; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -246,7 +248,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (sampledProfile != null) { writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); } - writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); if (jfrProfile != null) { writer.name(JsonKeys.JRF_PROFILE).value(logger, jfrProfile); } @@ -259,6 +261,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.endObject(); } + private @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) { + return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); + } + @Nullable @Override public Map getUnknown() { diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java index 6531d67e93e..512801bef1d 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -98,14 +98,6 @@ private void init() { profilingTracesHz); return; } - - // profiler = - // new AndroidProfiler( - // profilingTracesDirPath, - // (int) SECONDS.toMicros(1) / profilingTracesHz, - // frameMetricsCollector, - // null, - // logger); } @SuppressWarnings("ReferenceEquality") @@ -174,8 +166,7 @@ private void start() { filename = SentryUUID.generateSentryId() + ".jfr"; final String startData; try { - // System.out.println("### Starting profiler with start,jfr,event=wall,file"); - startData = profiler.execute("start,jfr,event=cpu,alloc,file=" + filename); + startData = profiler.execute("start,jfr,event=wall,file=" + filename); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java index 73f7909ae04..14a4b96a867 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/JfrSample.java @@ -7,6 +7,8 @@ import io.sentry.ObjectReader; import io.sentry.ObjectWriter; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; import org.jetbrains.annotations.NotNull; @@ -29,7 +31,8 @@ public static final class JsonKeys { @Override public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); - writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); + + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); writer.name(JsonKeys.STACK_ID).value(logger, stackId); if (threadId != null) { @@ -39,6 +42,10 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.endObject(); } + private @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) { + return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); + } + @Override public @Nullable Map getUnknown() { return new HashMap<>(); From 9cf7ef6d3994e30119c0551ce984f4d04f0aff40 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 8 Jul 2025 09:24:48 +0200 Subject: [PATCH 08/31] rename JfrProfile to SentryProfile --- .../src/main/java/io/sentry/ProfileChunk.java | 30 +++++------ .../java/io/sentry/SentryEnvelopeItem.java | 4 +- ...AsyncProfilerToSentryProfileConverter.java | 50 +++++++++++-------- .../{JfrProfile.java => SentryProfile.java} | 8 +-- 4 files changed, 51 insertions(+), 41 deletions(-) rename sentry/src/main/java/io/sentry/protocol/profiling/{JfrProfile.java => SentryProfile.java} (93%) diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index cd7c34619a9..5ea7bb158bd 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -4,7 +4,7 @@ import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import io.sentry.protocol.profiling.JfrProfile; +import io.sentry.protocol.profiling.SentryProfile; import io.sentry.vendor.gson.stream.JsonToken; import java.io.File; import java.io.IOException; @@ -36,7 +36,7 @@ public final class ProfileChunk implements JsonUnknown, JsonSerializable { /** Profile trace encoded with Base64. */ private @Nullable String sampledProfile = null; - private @Nullable JfrProfile jfrProfile; + private @Nullable SentryProfile sentryProfile; private @Nullable Map unknown; @@ -128,12 +128,12 @@ public double getTimestamp() { return version; } - public @Nullable JfrProfile getJfrProfile() { - return jfrProfile; + public @Nullable SentryProfile getJfrProfile() { + return sentryProfile; } - public void setJfrProfile(@Nullable JfrProfile jfrProfile) { - this.jfrProfile = jfrProfile; + public void setJfrProfile(@Nullable SentryProfile sentryProfile) { + this.sentryProfile = sentryProfile; } @Override @@ -152,7 +152,7 @@ public boolean equals(Object o) { && Objects.equals(version, that.version) && Objects.equals(sampledProfile, that.sampledProfile) && Objects.equals(unknown, that.unknown) - && Objects.equals(jfrProfile, that.jfrProfile); + && Objects.equals(sentryProfile, that.sentryProfile); } @Override @@ -168,7 +168,7 @@ public int hashCode() { environment, version, sampledProfile, - jfrProfile, + sentryProfile, unknown); } @@ -216,7 +216,7 @@ public static final class JsonKeys { public static final String VERSION = "version"; public static final String SAMPLED_PROFILE = "sampled_profile"; public static final String TIMESTAMP = "timestamp"; - public static final String JRF_PROFILE = "profile"; + public static final String SENTRY_PROFILE = "profile"; } @Override @@ -249,8 +249,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); } writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); - if (jfrProfile != null) { - writer.name(JsonKeys.JRF_PROFILE).value(logger, jfrProfile); + if (sentryProfile != null) { + writer.name(JsonKeys.SENTRY_PROFILE).value(logger, sentryProfile); } if (unknown != null) { for (String key : unknown.keySet()) { @@ -355,10 +355,10 @@ public static final class Deserializer implements JsonDeserializer data.timestamp = timestamp; } break; - case JsonKeys.JRF_PROFILE: - JfrProfile jfrProfile = reader.nextOrNull(logger, new JfrProfile.Deserializer()); - if (jfrProfile != null) { - data.jfrProfile = jfrProfile; + case JsonKeys.SENTRY_PROFILE: + SentryProfile sentryProfile = reader.nextOrNull(logger, new SentryProfile.Deserializer()); + if (sentryProfile != null) { + data.sentryProfile = sentryProfile; } break; default: diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 87acd2176d3..ed143246062 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -8,7 +8,7 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; -import io.sentry.protocol.profiling.JfrProfile; +import io.sentry.protocol.profiling.SentryProfile; // import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; @@ -298,7 +298,7 @@ private static void ensureAttachmentSizeLimit( if (traceFile.getName().endsWith(".jfr")) { // JfrProfile profile = new // JfrToSentryProfileConverter().convert(traceFile.toPath()); - JfrProfile profile = + SentryProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(traceFile.toPath()); profileChunk.setJfrProfile(profile); diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java index bcb64eb5566..ae3f339aff0 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -6,7 +6,7 @@ import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.StackTrace; import io.sentry.protocol.jfr.jfr.event.Event; -import io.sentry.protocol.profiling.JfrProfile; +import io.sentry.protocol.profiling.SentryProfile; import io.sentry.protocol.profiling.JfrSample; // import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.protocol.profiling.ThreadMetadata; @@ -20,7 +20,7 @@ import org.jetbrains.annotations.NotNull; public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { - private final @NotNull JfrProfile jfrProfile = new JfrProfile(); + private final @NotNull SentryProfile sentryProfile = new SentryProfile(); public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { super(jfr, args); @@ -31,7 +31,7 @@ public static void main(String[] args) throws IOException { Path jfrPath = Paths.get( "/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); - JfrProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); + SentryProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); // JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); System.out.println(profile.frames); System.out.println("Done"); @@ -58,41 +58,51 @@ public void visit(Event event, long value) { int[] locations = stackTrace.locations; if (args.threads) { - if (jfrProfile.threadMetadata == null) { - jfrProfile.threadMetadata = new HashMap<>(); + if (sentryProfile.threadMetadata == null) { + sentryProfile.threadMetadata = new HashMap<>(); } long threadIdToUse = jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; - if (jfrProfile.threadMetadata != null) { - jfrProfile.threadMetadata.computeIfAbsent( + if (sentryProfile.threadMetadata != null) { + final String threadName = getThreadName(event.tid); + // if(threadName.startsWith("AsyncProfiler-")) { + // // AsyncProfiler threads are not useful for profiling, so we + // skip them + // return; + // } + sentryProfile.threadMetadata.computeIfAbsent( String.valueOf(threadIdToUse), k -> { ThreadMetadata metadata = new ThreadMetadata(); - metadata.name = getThreadName(event.tid); + metadata.name = threadName; metadata.priority = 0; return metadata; }); } } - if (jfrProfile.samples == null) { - jfrProfile.samples = new ArrayList<>(); + if (sentryProfile.samples == null) { + sentryProfile.samples = new ArrayList<>(); } - if (jfrProfile.frames == null) { - jfrProfile.frames = new ArrayList<>(); + if (sentryProfile.frames == null) { + sentryProfile.frames = new ArrayList<>(); } List stack = new ArrayList<>(); int currentStack = stacks.size(); - int currentFrame = jfrProfile.frames != null ? jfrProfile.frames.size() : 0; + 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()); @@ -120,8 +130,8 @@ public void visit(Event event, long value) { frame.setLineno((element.getLineNumber() != 0) ? element.getLineNumber() : null); frame.setFilename(classNameWithLambdas); - if (jfrProfile.frames != null) { - jfrProfile.frames.add(frame); + if (sentryProfile.frames != null) { + sentryProfile.frames.add(frame); } stack.add(currentFrame); currentFrame++; @@ -144,19 +154,19 @@ public void visit(Event event, long value) { ? jfr.javaThreads.get(event.tid) : event.tid); sample.stackId = currentStack; - if (jfrProfile.samples != null) { - jfrProfile.samples.add(sample); + if (sentryProfile.samples != null) { + sentryProfile.samples.add(sample); } stacks.add(stack); } } }); - jfrProfile.stacks = stacks; + sentryProfile.stacks = stacks; System.out.println("Samples: " + events.size()); } - public static @NotNull JfrProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException { + public static @NotNull SentryProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException { JfrAsyncProfilerToSentryProfileConverter converter; try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { Arguments args = new Arguments(); @@ -170,6 +180,6 @@ public void visit(Event event, long value) { converter.convert(); } - return converter.jfrProfile; + return converter.sentryProfile; } } diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java similarity index 93% rename from sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java rename to sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java index d8aff8047db..ec872072702 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/JfrProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class JfrProfile implements JsonUnknown, JsonSerializable { +public final class SentryProfile implements JsonUnknown, JsonSerializable { public @Nullable List samples; public @Nullable List> stacks; // List of frame indices @@ -73,13 +73,13 @@ public static final class JsonKeys { public static final String THREAD_METADATA = "thread_metadata"; } - public static final class Deserializer implements JsonDeserializer { + public static final class Deserializer implements JsonDeserializer { @Override - public @NotNull JfrProfile deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + public @NotNull SentryProfile deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { reader.beginObject(); - JfrProfile data = new JfrProfile(); + SentryProfile data = new SentryProfile(); return data; // Map unknown = null; // From 5534204f85164093f580e97e4254b25523545034 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 8 Jul 2025 16:31:43 +0200 Subject: [PATCH 09/31] move java profiling into its own module, load using SPI --- buildSrc/src/main/java/Config.kt | 1 + .../api/sentry-async-profiler.api | 359 +++++++++++++++ ...yncProfilerContinuousProfilerProvider.java | 31 ++ ...AsyncProfilerProfileConverterProvider.java | 32 ++ .../protocol/jfr/convert/Arguments.java | 0 .../protocol/jfr/convert/CallStack.java | 0 .../protocol/jfr/convert/Classifier.java | 0 .../protocol/jfr/convert/FlameGraph.java | 0 .../io/sentry/protocol/jfr/convert/Frame.java | 0 .../io/sentry/protocol/jfr/convert/Index.java | 0 ...AsyncProfilerToSentryProfileConverter.java | 8 +- .../protocol/jfr/convert/JfrConverter.java | 0 .../protocol/jfr/convert/JfrToFlame.java | 0 .../jfr/convert/ResourceProcessor.java | 0 .../io/sentry/protocol/jfr/jfr/ClassRef.java | 0 .../sentry/protocol/jfr/jfr/Dictionary.java | 0 .../protocol/jfr/jfr/DictionaryInt.java | 0 .../io/sentry/protocol/jfr/jfr/Element.java | 0 .../io/sentry/protocol/jfr/jfr/JfrClass.java | 0 .../io/sentry/protocol/jfr/jfr/JfrField.java | 0 .../io/sentry/protocol/jfr/jfr/JfrReader.java | 0 .../io/sentry/protocol/jfr/jfr/MethodRef.java | 0 .../sentry/protocol/jfr/jfr/StackTrace.java | 0 .../jfr/jfr/event/AllocationSample.java | 0 .../protocol/jfr/jfr/event/CPULoad.java | 0 .../protocol/jfr/jfr/event/ContendedLock.java | 0 .../sentry/protocol/jfr/jfr/event/Event.java | 0 .../jfr/jfr/event/EventAggregator.java | 0 .../jfr/jfr/event/EventCollector.java | 0 .../jfr/jfr/event/ExecutionSample.java | 0 .../protocol/jfr/jfr/event/GCHeapSummary.java | 0 .../protocol/jfr/jfr/event/LiveObject.java | 0 .../protocol/jfr/jfr/event/MallocEvent.java | 0 .../jfr/jfr/event/MallocLeakAggregator.java | 0 .../protocol/jfr/jfr/event/ObjectCount.java | 0 .../profiling/JavaContinuousProfiler.java | 0 ...y.profiling.JavaContinuousProfilerProvider | 1 + ...try.profiling.JavaProfileConverterProvider | 1 + sentry/api/sentry.api | 412 ++---------------- sentry/build.gradle.kts | 2 - .../java/io/sentry/IProfileConverter.java | 26 ++ .../src/main/java/io/sentry/ProfileChunk.java | 7 +- sentry/src/main/java/io/sentry/Sentry.java | 10 +- .../java/io/sentry/SentryEnvelopeItem.java | 22 +- .../JavaContinuousProfilerProvider.java | 27 ++ .../JavaProfileConverterProvider.java | 21 + .../profiling/ProfilingServiceLoader.java | 76 ++++ .../test/java/io/sentry/JavaProfilerTest.kt | 31 -- settings.gradle.kts | 1 + 49 files changed, 647 insertions(+), 421 deletions(-) create mode 100644 sentry-async-profiler/api/sentry-async-profiler.api create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/Frame.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/Index.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java (97%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/Element.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java (100%) rename {sentry => sentry-async-profiler}/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java (100%) create mode 100644 sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider create mode 100644 sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider create mode 100644 sentry/src/main/java/io/sentry/IProfileConverter.java create mode 100644 sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java create mode 100644 sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java create mode 100644 sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java delete mode 100644 sentry/src/test/java/io/sentry/JavaProfilerTest.kt diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 69e8d02c892..6917c4fdf9a 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -78,6 +78,7 @@ object Config { val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp" val SENTRY_REACTOR_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.reactor" val SENTRY_KOTLIN_EXTENSIONS_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.kotlin-extensions" + val SENTRY_ASYNC_PROFILER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.async-profiler" val group = "io.sentry" val description = "SDK for sentry.io" val versionNameProp = "versionName" diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api new file mode 100644 index 00000000000..f5f43b794b4 --- /dev/null +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -0,0 +1,359 @@ +public final class io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider : io/sentry/profiling/JavaContinuousProfilerProvider { + public fun ()V + public fun getContinuousProfiler (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)Lio/sentry/IContinuousProfiler; +} + +public final class io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider : io/sentry/profiling/JavaProfileConverterProvider { + public fun ()V + public fun getProfileConverter ()Lio/sentry/IProfileConverter; +} + +public final class io/sentry/asyncprofiler/BuildConfig { + public static final field SENTRY_ASYNC_PROFILER_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/protocol/jfr/convert/Arguments { + public field alloc Z + public field bci Z + public field classify Z + public field cpu Z + public field dot Z + public field exclude Ljava/util/regex/Pattern; + public final field files Ljava/util/List; + public field from J + public field grain D + public field help Z + public field highlight Ljava/lang/String; + public field include Ljava/util/regex/Pattern; + public field inverted Z + public field leak Z + public field lines Z + public field live Z + public field lock Z + public field minwidth D + public field nativemem Z + public field norm Z + public field output Ljava/lang/String; + public field reverse Z + public field simple Z + public field skip I + public field state Ljava/lang/String; + public field threads Z + public field title Ljava/lang/String; + public field to J + public field total Z + public field wall Z + public fun ([Ljava/lang/String;)V +} + +public final class io/sentry/protocol/jfr/convert/CallStack { + public fun ()V + public fun clear ()V + public fun pop ()V + public fun push (Ljava/lang/String;B)V +} + +public final class io/sentry/protocol/jfr/convert/FlameGraph : java/util/Comparator { + public fun (Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun addSample (Lio/sentry/protocol/jfr/convert/CallStack;J)V + public fun compare (Lio/sentry/protocol/jfr/convert/Frame;Lio/sentry/protocol/jfr/convert/Frame;)I + public synthetic fun compare (Ljava/lang/Object;Ljava/lang/Object;)I + public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun dump (Ljava/io/PrintStream;)V + public fun parseCollapsed (Ljava/io/Reader;)V + public fun parseHtml (Ljava/io/Reader;)V +} + +public final class io/sentry/protocol/jfr/convert/Frame : java/util/HashMap { + public static final field TYPE_C1_COMPILED B + public static final field TYPE_CPP B + public static final field TYPE_INLINED B + public static final field TYPE_INTERPRETED B + public static final field TYPE_JIT_COMPILED B + public static final field TYPE_KERNEL B + public static final field TYPE_NATIVE B +} + +public final class io/sentry/protocol/jfr/convert/Index : java/util/HashMap { + public fun (Ljava/lang/Class;Ljava/lang/Object;)V + public fun (Ljava/lang/Class;Ljava/lang/Object;I)V + public fun index (Ljava/lang/Object;)I + public fun keys ()[Ljava/lang/Object; + public fun keys ([Ljava/lang/Object;)V +} + +public final class io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/protocol/jfr/convert/JfrConverter { + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; + public static fun main ([Ljava/lang/String;)V +} + +public abstract class io/sentry/protocol/jfr/convert/JfrConverter { + protected final field args Lio/sentry/protocol/jfr/convert/Arguments; + protected final field collector Lio/sentry/protocol/jfr/jfr/event/EventCollector; + protected final field jfr Lio/sentry/protocol/jfr/jfr/JfrReader; + protected field methodNames Lio/sentry/protocol/jfr/jfr/Dictionary; + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + protected fun collectEvents ()V + public fun convert ()V + protected fun convertChunk ()V + protected fun createCollector (Lio/sentry/protocol/jfr/convert/Arguments;)Lio/sentry/protocol/jfr/jfr/event/EventCollector; + public synthetic fun getCategory (Lio/sentry/protocol/jfr/jfr/StackTrace;)Lio/sentry/protocol/jfr/convert/Classifier$Category; + public fun getClassName (J)Ljava/lang/String; + public fun getMethodName (JB)Ljava/lang/String; + public fun getStackTraceElement (JBI)Ljava/lang/StackTraceElement; + public fun getThreadName (I)Ljava/lang/String; + protected fun getThreadStates (Z)Ljava/util/BitSet; + protected fun isNativeFrame (B)Z + protected fun toThreadState (Ljava/lang/String;)I + protected fun toTicks (J)J +} + +protected abstract class io/sentry/protocol/jfr/convert/JfrConverter$AggregatedEventVisitor : io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { + protected fun (Lio/sentry/protocol/jfr/convert/JfrConverter;)V + protected abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;J)V + public final fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V +} + +public final class io/sentry/protocol/jfr/convert/JfrToFlame : io/sentry/protocol/jfr/convert/JfrConverter { + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V + public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V + public fun dump (Ljava/io/OutputStream;)V +} + +public final class io/sentry/protocol/jfr/convert/ResourceProcessor { + public fun ()V + public static fun getResource (Ljava/lang/String;)Ljava/lang/String; + public static fun printTill (Ljava/io/PrintStream;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +} + +public final class io/sentry/protocol/jfr/jfr/ClassRef { + public final field name J + public fun (J)V +} + +public final class io/sentry/protocol/jfr/jfr/Dictionary { + public fun ()V + public fun (I)V + public fun clear ()V + public fun forEach (Lio/sentry/protocol/jfr/jfr/Dictionary$Visitor;)V + public fun get (J)Ljava/lang/Object; + public fun preallocate (I)I + public fun put (JLjava/lang/Object;)V + public fun size ()I +} + +public abstract interface class io/sentry/protocol/jfr/jfr/Dictionary$Visitor { + public abstract fun visit (JLjava/lang/Object;)V +} + +public final class io/sentry/protocol/jfr/jfr/DictionaryInt { + public fun ()V + public fun (I)V + public fun clear ()V + public fun forEach (Lio/sentry/protocol/jfr/jfr/DictionaryInt$Visitor;)V + public fun get (J)I + public fun get (JI)I + public fun preallocate (I)I + public fun put (JI)V +} + +public abstract interface class io/sentry/protocol/jfr/jfr/DictionaryInt$Visitor { + public abstract fun visit (JI)V +} + +public final class io/sentry/protocol/jfr/jfr/JfrClass { + public fun field (Ljava/lang/String;)Lio/sentry/protocol/jfr/jfr/JfrField; +} + +public final class io/sentry/protocol/jfr/jfr/JfrField { +} + +public final class io/sentry/protocol/jfr/jfr/JfrReader : java/io/Closeable { + public field chunkEndNanos J + public field chunkStartNanos J + public field chunkStartTicks J + public final field classes Lio/sentry/protocol/jfr/jfr/Dictionary; + public field endNanos J + public final field enums Ljava/util/Map; + public final field javaThreads Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field methods Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field settings Ljava/util/Map; + public final field stackTraces Lio/sentry/protocol/jfr/jfr/Dictionary; + public field startNanos J + public field startTicks J + public field stopAtNewChunk Z + public final field strings Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field symbols Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field threads Lio/sentry/protocol/jfr/jfr/Dictionary; + public field ticksPerSec J + public final field types Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field typesByName Ljava/util/Map; + public fun (Ljava/lang/String;)V + public fun (Ljava/nio/ByteBuffer;)V + public fun close ()V + public fun durationNanos ()J + public fun eof ()Z + public fun getBytes ()[B + public fun getDouble ()D + public fun getEnumKey (Ljava/lang/String;Ljava/lang/String;)I + public fun getEnumValue (Ljava/lang/String;I)Ljava/lang/String; + public fun getFloat ()F + public fun getString ()Ljava/lang/String; + public fun getVarint ()I + public fun getVarlong ()J + public fun hasMoreChunks ()Z + public fun incomplete ()Z + public fun readAllEvents ()Ljava/util/List; + public fun readAllEvents (Ljava/lang/Class;)Ljava/util/List; + public fun readEvent ()Lio/sentry/protocol/jfr/jfr/event/Event; + public fun readEvent (Ljava/lang/Class;)Lio/sentry/protocol/jfr/jfr/event/Event; + public fun registerEvent (Ljava/lang/String;Ljava/lang/Class;)V +} + +public final class io/sentry/protocol/jfr/jfr/MethodRef { + public final field cls J + public final field name J + public final field sig J + public fun (JJJ)V +} + +public final class io/sentry/protocol/jfr/jfr/StackTrace { + public final field locations [I + public final field methods [J + public final field types [B + public fun ([J[B[I)V +} + +public final class io/sentry/protocol/jfr/jfr/event/AllocationSample : io/sentry/protocol/jfr/jfr/event/Event { + public final field allocationSize J + public final field classId I + public final field tlabSize J + public fun (JIIIJJ)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/CPULoad : io/sentry/protocol/jfr/jfr/event/Event { + public final field jvmSystem F + public final field jvmUser F + public final field machineTotal F + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ContendedLock : io/sentry/protocol/jfr/jfr/event/Event { + public final field classId I + public final field duration J + public fun (JIIJI)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public abstract class io/sentry/protocol/jfr/jfr/event/Event : java/lang/Comparable { + public final field stackTraceId I + public final field tid I + public final field time J + protected fun (JII)V + public fun classId ()J + public fun compareTo (Lio/sentry/protocol/jfr/jfr/event/Event;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun samples ()J + public fun toString ()Ljava/lang/String; + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/EventAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { + public fun (ZD)V + public fun afterChunk ()V + public fun beforeChunk ()V + public fun coarsen (D)V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V + public fun finish ()Z + public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V + public fun size ()I +} + +public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector { + public abstract fun afterChunk ()V + public abstract fun beforeChunk ()V + public abstract fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public abstract fun finish ()Z + public abstract fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V +} + +public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { + public abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ExecutionSample : io/sentry/protocol/jfr/jfr/event/Event { + public final field samples I + public final field threadState I + public fun (JIIII)V + public fun samples ()J + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/GCHeapSummary : io/sentry/protocol/jfr/jfr/event/Event { + public final field afterGC Z + public final field committed J + public final field gcId I + public final field reserved J + public final field used J + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/LiveObject : io/sentry/protocol/jfr/jfr/event/Event { + public final field allocationSize J + public final field allocationTime J + public final field classId I + public fun (JIIIJJ)V + public fun classId ()J + public fun hashCode ()I + public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/MallocEvent : io/sentry/protocol/jfr/jfr/event/Event { + public final field address J + public final field size J + public fun (JIIJJ)V + public fun value ()J +} + +public final class io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { + public fun (Lio/sentry/protocol/jfr/jfr/event/EventCollector;)V + public fun afterChunk ()V + public fun beforeChunk ()V + public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public fun finish ()Z + public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V +} + +public final class io/sentry/protocol/jfr/jfr/event/ObjectCount : io/sentry/protocol/jfr/jfr/event/Event { + public final field classId I + public final field count J + public final field gcId I + public final field totalSize J + public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V +} + +public final class io/sentry/protocol/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { + public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V + public fun close (Z)V + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRootSpanCounter ()I + public fun isRunning ()Z + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V + public fun reevaluateSampling ()V + public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfiler (Lio/sentry/ProfileLifecycle;)V +} + diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java new file mode 100644 index 00000000000..abd112537e4 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java @@ -0,0 +1,31 @@ +package io.sentry.asyncprofiler; + +import io.sentry.IContinuousProfiler; +import io.sentry.ILogger; +import io.sentry.ISentryExecutorService; +import io.sentry.profiling.JavaContinuousProfilerProvider; +import io.sentry.profiling.JavaProfileConverterProvider; +import io.sentry.protocol.profiling.JavaContinuousProfiler; +import org.jetbrains.annotations.NotNull; + +/** + * AsyncProfiler implementation of {@link JavaContinuousProfilerProvider} and {@link + * JavaProfileConverterProvider}. This provider integrates AsyncProfiler with Sentry's continuous + * profiling system and provides profile conversion functionality. + */ +public final class AsyncProfilerContinuousProfilerProvider + implements JavaContinuousProfilerProvider { + + @Override + public @NotNull IContinuousProfiler getContinuousProfiler( + ILogger logger, + String profilingTracesDirPath, + int profilingTracesHz, + ISentryExecutorService executorService) { + return new JavaContinuousProfiler( + logger, + profilingTracesDirPath, + 10, // default profilingTracesHz + executorService); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java new file mode 100644 index 00000000000..869dd477388 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java @@ -0,0 +1,32 @@ +package io.sentry.asyncprofiler; + +import io.sentry.IProfileConverter; +import io.sentry.profiling.JavaProfileConverterProvider; +import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * AsyncProfiler implementation of {@link JavaProfileConverterProvider}. This provider integrates + * AsyncProfiler's JFR converter with Sentry's profiling system. + */ +public final class AsyncProfilerProfileConverterProvider implements JavaProfileConverterProvider { + + @Override + public @Nullable IProfileConverter getProfileConverter() { + return new AsyncProfilerProfileConverter(); + } + + /** + * Internal implementation of IProfileConverter that delegates to + * JfrAsyncProfilerToSentryProfileConverter. + */ + private static final class AsyncProfilerProfileConverter implements IProfileConverter { + + @Override + public @NotNull io.sentry.protocol.profiling.SentryProfile convertFromFile( + @NotNull java.nio.file.Path jfrFilePath) throws java.io.IOException { + return JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(jfrFilePath); + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Frame.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/Frame.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Frame.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/Index.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java similarity index 97% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java index ae3f339aff0..9f0c807dc87 100644 --- a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -6,9 +6,8 @@ import io.sentry.protocol.jfr.jfr.JfrReader; import io.sentry.protocol.jfr.jfr.StackTrace; import io.sentry.protocol.jfr.jfr.event.Event; -import io.sentry.protocol.profiling.SentryProfile; import io.sentry.protocol.profiling.JfrSample; -// import io.sentry.protocol.profiling.JfrToSentryProfileConverter; +import io.sentry.protocol.profiling.SentryProfile; import io.sentry.protocol.profiling.ThreadMetadata; import java.io.IOException; import java.nio.file.Path; @@ -31,7 +30,7 @@ public static void main(String[] args) throws IOException { Path jfrPath = Paths.get( "/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); - SentryProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFile(jfrPath); + SentryProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(jfrPath); // JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); System.out.println(profile.frames); System.out.println("Done"); @@ -166,7 +165,8 @@ public void visit(Event event, long value) { System.out.println("Samples: " + events.size()); } - public static @NotNull SentryProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException { + public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath) + throws IOException { JfrAsyncProfilerToSentryProfileConverter converter; try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { Arguments args = new Arguments(); diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Element.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/Element.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Element.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java diff --git a/sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java similarity index 100% rename from sentry/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java rename to sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java diff --git a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider new file mode 100644 index 00000000000..7d792ae8ff2 --- /dev/null +++ b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider @@ -0,0 +1 @@ +io.sentry.asyncprofiler.AsyncProfilerContinuousProfilerProvider \ No newline at end of file diff --git a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider new file mode 100644 index 00000000000..bd97c6688d6 --- /dev/null +++ b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider @@ -0,0 +1 @@ +io.sentry.asyncprofiler.AsyncProfilerProfileConverterProvider \ No newline at end of file diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3d8f9ffdf85..abcee7cd62f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -812,6 +812,10 @@ public abstract interface class io/sentry/IPerformanceSnapshotCollector : io/sen public abstract fun setup ()V } +public abstract interface class io/sentry/IProfileConverter { + public abstract fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; +} + public abstract interface class io/sentry/IReplayApi { public abstract fun disableDebugMaskingOverlay ()V public abstract fun enableDebugMaskingOverlay ()V @@ -1934,12 +1938,12 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; public fun getEnvironment ()Ljava/lang/String; - public fun getJfrProfile ()Lio/sentry/protocol/profiling/JfrProfile; public fun getMeasurements ()Ljava/util/Map; public fun getPlatform ()Ljava/lang/String; public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getRelease ()Ljava/lang/String; public fun getSampledProfile ()Ljava/lang/String; + public fun getSentryProfile ()Lio/sentry/protocol/profiling/SentryProfile; public fun getTimestamp ()D public fun getTraceFile ()Ljava/io/File; public fun getUnknown ()Ljava/util/Map; @@ -1947,8 +1951,8 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr public fun hashCode ()I public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V - public fun setJfrProfile (Lio/sentry/protocol/profiling/JfrProfile;)V public fun setSampledProfile (Ljava/lang/String;)V + public fun setSentryProfile (Lio/sentry/protocol/profiling/SentryProfile;)V public fun setUnknown (Ljava/util/Map;)V } @@ -1968,12 +1972,12 @@ public final class io/sentry/ProfileChunk$JsonKeys { public static final field CLIENT_SDK Ljava/lang/String; public static final field DEBUG_META Ljava/lang/String; public static final field ENVIRONMENT Ljava/lang/String; - public static final field JRF_PROFILE Ljava/lang/String; public static final field MEASUREMENTS Ljava/lang/String; public static final field PLATFORM Ljava/lang/String; public static final field PROFILER_ID Ljava/lang/String; public static final field RELEASE Ljava/lang/String; public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field SENTRY_PROFILE Ljava/lang/String; public static final field TIMESTAMP Ljava/lang/String; public static final field VERSION Ljava/lang/String; public fun ()V @@ -4933,6 +4937,20 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKey public fun ()V } +public abstract interface class io/sentry/profiling/JavaContinuousProfilerProvider { + public abstract fun getContinuousProfiler (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)Lio/sentry/IContinuousProfiler; +} + +public abstract interface class io/sentry/profiling/JavaProfileConverterProvider { + public abstract fun getProfileConverter ()Lio/sentry/IProfileConverter; +} + +public final class io/sentry/profiling/ProfilingServiceLoader { + public fun ()V + public static fun loadContinuousProfiler (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)Lio/sentry/IContinuousProfiler; + public static fun loadProfileConverter ()Lio/sentry/IProfileConverter; +} + public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V @@ -6187,350 +6205,6 @@ public final class io/sentry/protocol/ViewHierarchyNode$JsonKeys { public fun ()V } -public final class io/sentry/protocol/jfr/convert/Arguments { - public field alloc Z - public field bci Z - public field classify Z - public field cpu Z - public field dot Z - public field exclude Ljava/util/regex/Pattern; - public final field files Ljava/util/List; - public field from J - public field grain D - public field help Z - public field highlight Ljava/lang/String; - public field include Ljava/util/regex/Pattern; - public field inverted Z - public field leak Z - public field lines Z - public field live Z - public field lock Z - public field minwidth D - public field nativemem Z - public field norm Z - public field output Ljava/lang/String; - public field reverse Z - public field simple Z - public field skip I - public field state Ljava/lang/String; - public field threads Z - public field title Ljava/lang/String; - public field to J - public field total Z - public field wall Z - public fun ([Ljava/lang/String;)V -} - -public final class io/sentry/protocol/jfr/convert/CallStack { - public fun ()V - public fun clear ()V - public fun pop ()V - public fun push (Ljava/lang/String;B)V -} - -public final class io/sentry/protocol/jfr/convert/FlameGraph : java/util/Comparator { - public fun (Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun addSample (Lio/sentry/protocol/jfr/convert/CallStack;J)V - public fun compare (Lio/sentry/protocol/jfr/convert/Frame;Lio/sentry/protocol/jfr/convert/Frame;)I - public synthetic fun compare (Ljava/lang/Object;Ljava/lang/Object;)I - public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun dump (Ljava/io/PrintStream;)V - public fun parseCollapsed (Ljava/io/Reader;)V - public fun parseHtml (Ljava/io/Reader;)V -} - -public final class io/sentry/protocol/jfr/convert/Frame : java/util/HashMap { - public static final field TYPE_C1_COMPILED B - public static final field TYPE_CPP B - public static final field TYPE_INLINED B - public static final field TYPE_INTERPRETED B - public static final field TYPE_JIT_COMPILED B - public static final field TYPE_KERNEL B - public static final field TYPE_NATIVE B -} - -public final class io/sentry/protocol/jfr/convert/Index : java/util/HashMap { - public fun (Ljava/lang/Class;Ljava/lang/Object;)V - public fun (Ljava/lang/Class;Ljava/lang/Object;I)V - public fun index (Ljava/lang/Object;)I - public fun keys ()[Ljava/lang/Object; - public fun keys ([Ljava/lang/Object;)V -} - -public final class io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/protocol/jfr/convert/JfrConverter { - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V - public static fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/JfrProfile; - public static fun main ([Ljava/lang/String;)V -} - -public abstract class io/sentry/protocol/jfr/convert/JfrConverter { - protected final field args Lio/sentry/protocol/jfr/convert/Arguments; - protected final field collector Lio/sentry/protocol/jfr/jfr/event/EventCollector; - protected final field jfr Lio/sentry/protocol/jfr/jfr/JfrReader; - protected field methodNames Lio/sentry/protocol/jfr/jfr/Dictionary; - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V - protected fun collectEvents ()V - public fun convert ()V - protected fun convertChunk ()V - protected fun createCollector (Lio/sentry/protocol/jfr/convert/Arguments;)Lio/sentry/protocol/jfr/jfr/event/EventCollector; - public synthetic fun getCategory (Lio/sentry/protocol/jfr/jfr/StackTrace;)Lio/sentry/protocol/jfr/convert/Classifier$Category; - public fun getClassName (J)Ljava/lang/String; - public fun getMethodName (JB)Ljava/lang/String; - public fun getStackTraceElement (JBI)Ljava/lang/StackTraceElement; - public fun getThreadName (I)Ljava/lang/String; - protected fun getThreadStates (Z)Ljava/util/BitSet; - protected fun isNativeFrame (B)Z - protected fun toThreadState (Ljava/lang/String;)I - protected fun toTicks (J)J -} - -protected abstract class io/sentry/protocol/jfr/convert/JfrConverter$AggregatedEventVisitor : io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { - protected fun (Lio/sentry/protocol/jfr/convert/JfrConverter;)V - protected abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;J)V - public final fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V -} - -public final class io/sentry/protocol/jfr/convert/JfrToFlame : io/sentry/protocol/jfr/convert/JfrConverter { - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V - public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun dump (Ljava/io/OutputStream;)V -} - -public final class io/sentry/protocol/jfr/convert/ResourceProcessor { - public fun ()V - public static fun getResource (Ljava/lang/String;)Ljava/lang/String; - public static fun printTill (Ljava/io/PrintStream;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -} - -public final class io/sentry/protocol/jfr/jfr/ClassRef { - public final field name J - public fun (J)V -} - -public final class io/sentry/protocol/jfr/jfr/Dictionary { - public fun ()V - public fun (I)V - public fun clear ()V - public fun forEach (Lio/sentry/protocol/jfr/jfr/Dictionary$Visitor;)V - public fun get (J)Ljava/lang/Object; - public fun preallocate (I)I - public fun put (JLjava/lang/Object;)V - public fun size ()I -} - -public abstract interface class io/sentry/protocol/jfr/jfr/Dictionary$Visitor { - public abstract fun visit (JLjava/lang/Object;)V -} - -public final class io/sentry/protocol/jfr/jfr/DictionaryInt { - public fun ()V - public fun (I)V - public fun clear ()V - public fun forEach (Lio/sentry/protocol/jfr/jfr/DictionaryInt$Visitor;)V - public fun get (J)I - public fun get (JI)I - public fun preallocate (I)I - public fun put (JI)V -} - -public abstract interface class io/sentry/protocol/jfr/jfr/DictionaryInt$Visitor { - public abstract fun visit (JI)V -} - -public final class io/sentry/protocol/jfr/jfr/JfrClass { - public fun field (Ljava/lang/String;)Lio/sentry/protocol/jfr/jfr/JfrField; -} - -public final class io/sentry/protocol/jfr/jfr/JfrField { -} - -public final class io/sentry/protocol/jfr/jfr/JfrReader : java/io/Closeable { - public field chunkEndNanos J - public field chunkStartNanos J - public field chunkStartTicks J - public final field classes Lio/sentry/protocol/jfr/jfr/Dictionary; - public field endNanos J - public final field enums Ljava/util/Map; - public final field javaThreads Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field methods Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field settings Ljava/util/Map; - public final field stackTraces Lio/sentry/protocol/jfr/jfr/Dictionary; - public field startNanos J - public field startTicks J - public field stopAtNewChunk Z - public final field strings Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field symbols Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field threads Lio/sentry/protocol/jfr/jfr/Dictionary; - public field ticksPerSec J - public final field types Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field typesByName Ljava/util/Map; - public fun (Ljava/lang/String;)V - public fun (Ljava/nio/ByteBuffer;)V - public fun close ()V - public fun durationNanos ()J - public fun eof ()Z - public fun getBytes ()[B - public fun getDouble ()D - public fun getEnumKey (Ljava/lang/String;Ljava/lang/String;)I - public fun getEnumValue (Ljava/lang/String;I)Ljava/lang/String; - public fun getFloat ()F - public fun getString ()Ljava/lang/String; - public fun getVarint ()I - public fun getVarlong ()J - public fun hasMoreChunks ()Z - public fun incomplete ()Z - public fun readAllEvents ()Ljava/util/List; - public fun readAllEvents (Ljava/lang/Class;)Ljava/util/List; - public fun readEvent ()Lio/sentry/protocol/jfr/jfr/event/Event; - public fun readEvent (Ljava/lang/Class;)Lio/sentry/protocol/jfr/jfr/event/Event; - public fun registerEvent (Ljava/lang/String;Ljava/lang/Class;)V -} - -public final class io/sentry/protocol/jfr/jfr/MethodRef { - public final field cls J - public final field name J - public final field sig J - public fun (JJJ)V -} - -public final class io/sentry/protocol/jfr/jfr/StackTrace { - public final field locations [I - public final field methods [J - public final field types [B - public fun ([J[B[I)V -} - -public final class io/sentry/protocol/jfr/jfr/event/AllocationSample : io/sentry/protocol/jfr/jfr/event/Event { - public final field allocationSize J - public final field classId I - public final field tlabSize J - public fun (JIIIJJ)V - public fun classId ()J - public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z - public fun value ()J -} - -public final class io/sentry/protocol/jfr/jfr/event/CPULoad : io/sentry/protocol/jfr/jfr/event/Event { - public final field jvmSystem F - public final field jvmUser F - public final field machineTotal F - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V -} - -public final class io/sentry/protocol/jfr/jfr/event/ContendedLock : io/sentry/protocol/jfr/jfr/event/Event { - public final field classId I - public final field duration J - public fun (JIIJI)V - public fun classId ()J - public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z - public fun value ()J -} - -public abstract class io/sentry/protocol/jfr/jfr/event/Event : java/lang/Comparable { - public final field stackTraceId I - public final field tid I - public final field time J - protected fun (JII)V - public fun classId ()J - public fun compareTo (Lio/sentry/protocol/jfr/jfr/event/Event;)I - public synthetic fun compareTo (Ljava/lang/Object;)I - public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z - public fun samples ()J - public fun toString ()Ljava/lang/String; - public fun value ()J -} - -public final class io/sentry/protocol/jfr/jfr/event/EventAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { - public fun (ZD)V - public fun afterChunk ()V - public fun beforeChunk ()V - public fun coarsen (D)V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V - public fun finish ()Z - public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V - public fun size ()I -} - -public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector { - public abstract fun afterChunk ()V - public abstract fun beforeChunk ()V - public abstract fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V - public abstract fun finish ()Z - public abstract fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V -} - -public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { - public abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V -} - -public final class io/sentry/protocol/jfr/jfr/event/ExecutionSample : io/sentry/protocol/jfr/jfr/event/Event { - public final field samples I - public final field threadState I - public fun (JIIII)V - public fun samples ()J - public fun value ()J -} - -public final class io/sentry/protocol/jfr/jfr/event/GCHeapSummary : io/sentry/protocol/jfr/jfr/event/Event { - public final field afterGC Z - public final field committed J - public final field gcId I - public final field reserved J - public final field used J - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V -} - -public final class io/sentry/protocol/jfr/jfr/event/LiveObject : io/sentry/protocol/jfr/jfr/event/Event { - public final field allocationSize J - public final field allocationTime J - public final field classId I - public fun (JIIIJJ)V - public fun classId ()J - public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z - public fun value ()J -} - -public final class io/sentry/protocol/jfr/jfr/event/MallocEvent : io/sentry/protocol/jfr/jfr/event/Event { - public final field address J - public final field size J - public fun (JIIJJ)V - public fun value ()J -} - -public final class io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { - public fun (Lio/sentry/protocol/jfr/jfr/event/EventCollector;)V - public fun afterChunk ()V - public fun beforeChunk ()V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V - public fun finish ()Z - public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V -} - -public final class io/sentry/protocol/jfr/jfr/event/ObjectCount : io/sentry/protocol/jfr/jfr/event/Event { - public final field classId I - public final field count J - public final field gcId I - public final field totalSize J - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V -} - -public final class io/sentry/protocol/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { - public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V - public fun close (Z)V - public fun getProfilerId ()Lio/sentry/protocol/SentryId; - public fun getRootSpanCounter ()I - public fun isRunning ()Z - public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V - public fun reevaluateSampling ()V - public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V - public fun stopProfiler (Lio/sentry/ProfileLifecycle;)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; @@ -6552,51 +6226,51 @@ public final class io/sentry/protocol/profiling/JfrFrame$JsonKeys { public fun ()V } -public final class io/sentry/protocol/profiling/JfrProfile : 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/JfrSample : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public field stackId I + public field threadId Ljava/lang/String; + public field timestamp D 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/JfrProfile$Deserializer : io/sentry/JsonDeserializer { +public final class io/sentry/protocol/profiling/JfrSample$Deserializer : io/sentry/JsonDeserializer { public fun ()V - public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/JfrProfile; + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/profiling/JfrSample; public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } -public final class io/sentry/protocol/profiling/JfrProfile$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/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 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 field frames Ljava/util/List; + public field samples Ljava/util/List; + public field stacks Ljava/util/List; + public field threadMetadata Ljava/util/Map; 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/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 } diff --git a/sentry/build.gradle.kts b/sentry/build.gradle.kts index 4dc477f4a27..6a5a2182a89 100644 --- a/sentry/build.gradle.kts +++ b/sentry/build.gradle.kts @@ -21,8 +21,6 @@ dependencies { errorprone(libs.nullaway) compileOnly(libs.jetbrains.annotations) compileOnly(libs.nopen.annotations) - // https://mvnrepository.com/artifact/tools.profiler/async-profiler - implementation("tools.profiler:async-profiler:3.0") // tests testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(libs.awaitility.kotlin) diff --git a/sentry/src/main/java/io/sentry/IProfileConverter.java b/sentry/src/main/java/io/sentry/IProfileConverter.java new file mode 100644 index 00000000000..9b594c2dbf5 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IProfileConverter.java @@ -0,0 +1,26 @@ +package io.sentry; + +import io.sentry.protocol.profiling.SentryProfile; +import java.io.IOException; +import java.nio.file.Path; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Interface for converting JFR (Java Flight Recorder) files to Sentry profiles. This abstraction + * allows different profiling implementations to be used without direct dependencies between + * modules. + */ +@ApiStatus.Internal +public interface IProfileConverter { + + /** + * Converts a JFR file to a SentryProfile. + * + * @param jfrFilePath The path to the JFR file to convert + * @return The converted SentryProfile + * @throws IOException If an error occurs while reading or converting the file + */ + @NotNull + SentryProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException; +} diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 5ea7bb158bd..40876f4d10b 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -128,11 +128,11 @@ public double getTimestamp() { return version; } - public @Nullable SentryProfile getJfrProfile() { + public @Nullable SentryProfile getSentryProfile() { return sentryProfile; } - public void setJfrProfile(@Nullable SentryProfile sentryProfile) { + public void setSentryProfile(@Nullable SentryProfile sentryProfile) { this.sentryProfile = sentryProfile; } @@ -356,7 +356,8 @@ public static final class Deserializer implements JsonDeserializer } break; case JsonKeys.SENTRY_PROFILE: - SentryProfile sentryProfile = reader.nextOrNull(logger, new SentryProfile.Deserializer()); + SentryProfile sentryProfile = + reader.nextOrNull(logger, new SentryProfile.Deserializer()); if (sentryProfile != null) { data.sentryProfile = sentryProfile; } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index d22ed360a0b..98ffc1ec0e0 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -15,10 +15,10 @@ import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.logger.ILoggerApi; import io.sentry.opentelemetry.OpenTelemetryUtil; +import io.sentry.profiling.ProfilingServiceLoader; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; -import io.sentry.protocol.profiling.JavaContinuousProfiler; import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.DebugMetaPropertiesApplier; @@ -667,9 +667,11 @@ private static void initConfigurations(final @NotNull SentryOptions options) { } // TODO: make this configurable - if (options.isContinuousProfilingEnabled()) { - options.setContinuousProfiler( - new JavaContinuousProfiler(new SystemOutLogger(), "", 10, options.getExecutorService())); + if (options.isContinuousProfilingEnabled() && profilingTracesDirPath != null) { + final IContinuousProfiler continuousProfiler = + ProfilingServiceLoader.loadContinuousProfiler( + new SystemOutLogger(), profilingTracesDirPath, 10, options.getExecutorService()); + options.setContinuousProfiler(continuousProfiler); } options diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index ed143246062..6a3e5e91804 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -6,10 +6,9 @@ import io.sentry.clientreport.ClientReport; import io.sentry.exception.SentryEnvelopeException; +import io.sentry.profiling.ProfilingServiceLoader; import io.sentry.protocol.SentryTransaction; -import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; import io.sentry.protocol.profiling.SentryProfile; -// import io.sentry.protocol.profiling.JfrToSentryProfileConverter; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; @@ -295,13 +294,20 @@ private static void ensureAttachmentSizeLimit( "Dropping profile chunk, because the file '%s' doesn't exists", traceFile.getName())); } - if (traceFile.getName().endsWith(".jfr")) { - // JfrProfile profile = new - // JfrToSentryProfileConverter().convert(traceFile.toPath()); - SentryProfile profile = - JfrAsyncProfilerToSentryProfileConverter.convertFromFile(traceFile.toPath()); - profileChunk.setJfrProfile(profile); + if (traceFile.getName().endsWith(".jfr")) { + final IProfileConverter profileConverter = + ProfilingServiceLoader.loadProfileConverter(); + if (profileConverter != null) { + try { + final SentryProfile profile = + profileConverter.convertFromFile(traceFile.toPath()); + profileChunk.setSentryProfile(profile); + } catch (IOException e) { + throw new SentryEnvelopeException("Profile conversion failed"); + } + } + // If no converter is available, JFR profile conversion is skipped } else { // The payload of the profile item is a json including the trace file encoded with // base64 diff --git a/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java new file mode 100644 index 00000000000..ffc779a02dd --- /dev/null +++ b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java @@ -0,0 +1,27 @@ +package io.sentry.profiling; + +import io.sentry.IContinuousProfiler; +import io.sentry.ILogger; +import io.sentry.ISentryExecutorService; +import org.jetbrains.annotations.NotNull; + +/** + * Service provider interface for creating continuous profilers. + * + *

This interface allows for pluggable continuous profiler implementations that can be discovered + * at runtime using the ServiceLoader mechanism. + */ +public interface JavaContinuousProfilerProvider { + + /** + * Creates and returns a continuous profiler instance. + * + * @return a continuous profiler instance, or null if the provider cannot create one + */ + @NotNull + IContinuousProfiler getContinuousProfiler( + ILogger logger, + String profilingTracesDirPath, + int profilingTracesHz, + ISentryExecutorService executorService); +} diff --git a/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java new file mode 100644 index 00000000000..34ac31c66f0 --- /dev/null +++ b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java @@ -0,0 +1,21 @@ +package io.sentry.profiling; + +import io.sentry.IProfileConverter; +import org.jetbrains.annotations.Nullable; + +/** + * Service provider interface for creating profile converters. + * + *

This interface allows for pluggable profile converter implementations that can be discovered + * at runtime using the ServiceLoader mechanism. + */ +public interface JavaProfileConverterProvider { + + /** + * Creates and returns a profile converter instance. + * + * @return a profile converter instance, or null if the provider cannot create one + */ + @Nullable + IProfileConverter getProfileConverter(); +} diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java new file mode 100644 index 00000000000..ec09de6075f --- /dev/null +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -0,0 +1,76 @@ +package io.sentry.profiling; + +import io.sentry.IContinuousProfiler; +import io.sentry.ILogger; +import io.sentry.IProfileConverter; +import io.sentry.ISentryExecutorService; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.SentryLevel; +import java.util.Iterator; +import java.util.ServiceLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class ProfilingServiceLoader { + + public static @NotNull IContinuousProfiler loadContinuousProfiler( + ILogger logger, + String profilingTracesDirPath, + int profilingTracesHz, + ISentryExecutorService executorService) { + try { + JavaContinuousProfilerProvider provider = + loadSingleProvider(JavaContinuousProfilerProvider.class); + + if (provider != null) { + logger.log( + SentryLevel.DEBUG, + "Loaded continuous profiler from provider: %s", + provider.getClass().getName()); + return provider.getContinuousProfiler( + logger, profilingTracesDirPath, profilingTracesHz, executorService); + } + + logger.log( + SentryLevel.DEBUG, "No continuous profiler provider found, using NoOpContinuousProfiler"); + return NoOpContinuousProfiler.getInstance(); + } catch (Throwable t) { + logger.log( + SentryLevel.ERROR, + "Failed to load continuous profiler provider, using NoOpContinuousProfiler", + t); + return NoOpContinuousProfiler.getInstance(); + } + } + + /** + * Loads a profile converter using ServiceLoader discovery pattern. + * + * @return an IProfileConverter instance or null if no provider is found + */ + public static @Nullable IProfileConverter loadProfileConverter() { + try { + JavaProfileConverterProvider provider = + loadSingleProvider(JavaProfileConverterProvider.class); + if (provider != null) { + return provider.getProfileConverter(); + } else { + return null; + } + } catch (Throwable t) { + // Log error and return null to skip conversion + return null; + } + } + + private static @Nullable T loadSingleProvider(Class clazz) { + final ServiceLoader serviceLoader = ServiceLoader.load(clazz); + final Iterator iterator = serviceLoader.iterator(); + + if (iterator.hasNext()) { + return iterator.next(); + } else { + return null; + } + } +} diff --git a/sentry/src/test/java/io/sentry/JavaProfilerTest.kt b/sentry/src/test/java/io/sentry/JavaProfilerTest.kt deleted file mode 100644 index 25f0b07432c..00000000000 --- a/sentry/src/test/java/io/sentry/JavaProfilerTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.sentry - -import kotlin.test.Test -import one.profiler.AsyncProfiler - -class JavaProfilerTest { - - private class Fixture { - val contentType = "application/json" - val filename = "logs.txt" - val bytes = "content".toByteArray() - val pathname = "path/to/$filename" - } - - private val fixture = Fixture() - - @Test - fun `testprofilerone`() { - val profiler = AsyncProfiler.getInstance() - val startResult = profiler.execute("start,jfr,event=wall,alloc,loop=5s,file=test88-%t.jfr") - println(startResult) - - for (i in 1..20) { - println(i) - Thread.sleep(100) - } - - var endResult = profiler.execute("stop,jfr,file=myNewFile.jfr") - println(endResult) - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7081c58fbe5..967f9e6eb48 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,6 +62,7 @@ include( "sentry-quartz", "sentry-okhttp", "sentry-reactor", + "sentry-async-profiler", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-console-opentelemetry-noagent", From cabc384a7eeadfd3d32c824476565140b3ed8ddd Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 11 Jul 2025 13:59:24 +0200 Subject: [PATCH 10/31] [WIP] use getProfilingTracesDirPath --- sentry/src/main/java/io/sentry/SentryOptions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 10e4cce0f14..bd829a66e10 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -2028,7 +2028,8 @@ public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { public @Nullable String getProfilingTracesDirPath() { final String cacheDirPath = getCacheDirPath(); if (cacheDirPath == null) { - return null; + // TODO: Should we add ExternalOptions to let users define the tracesDirPath? + return new File(".", "profiling_traces").getAbsolutePath(); } return new File(cacheDirPath, "profiling_traces").getAbsolutePath(); } From ec59901af39c0decbc1c9836eaba18c3fee6c7b3 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 11 Jul 2025 14:00:30 +0200 Subject: [PATCH 11/31] add missing build.gradle.kts for profiler module --- sentry-async-profiler/build.gradle.kts | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 sentry-async-profiler/build.gradle.kts diff --git a/sentry-async-profiler/build.gradle.kts b/sentry-async-profiler/build.gradle.kts new file mode 100644 index 00000000000..0f4628e3e06 --- /dev/null +++ b/sentry-async-profiler/build.gradle.kts @@ -0,0 +1,90 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id("io.sentry.javadoc") + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() +} + +kotlin { explicitApi() } + +dependencies { + api(projects.sentry) + + implementation("tools.profiler:async-profiler:3.0") + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentryTestSupport) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.asyncprofiler") + buildConfigField( + "String", + "SENTRY_ASYNC_PROFILER_SDK_NAME", + "\"${Config.Sentry.SENTRY_ASYNC_PROFILER_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_ASYNC_PROFILER_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-async-profiler", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} From 543fc408950f40f53d8a0018676d70d0cc20f84a Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 11 Jul 2025 15:57:45 +0200 Subject: [PATCH 12/31] WIP continuous profiling in trace mode --- .../profiling/JavaContinuousProfiler.java | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java index 512801bef1d..847ed8830b5 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java @@ -2,6 +2,7 @@ import static io.sentry.DataCategory.All; import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; +import static java.util.concurrent.TimeUnit.SECONDS; import io.sentry.DataCategory; import io.sentry.IContinuousProfiler; @@ -55,6 +56,7 @@ public final class JavaContinuousProfiler private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); + private final @NotNull String profilingIntervalMicros; private @NotNull String filename = ""; @@ -77,6 +79,8 @@ public JavaContinuousProfiler( this.profilingTracesHz = profilingTracesHz; this.executorService = executorService; this.profiler = AsyncProfiler.getInstance(); + this.profilingIntervalMicros = + String.format("%dus", (int) SECONDS.toMicros(1) / profilingTracesHz); } private void init() { @@ -108,14 +112,34 @@ public void startProfiler( try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (shouldSample) { isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); - // Kepp TRUE for now - // shouldSample = false; + shouldSample = false; } if (!isSampled) { logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); return; } + switch (profileLifecycle) { + case TRACE: + // rootSpanCounter should never be negative, unless the user changed profile lifecycle + // while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + rootSpanCounter++; + break; + case MANUAL: + // We check if the profiler is already running and log a message only in manual mode, + // since + // in trace mode we can have multiple concurrent traces + if (isRunning()) { + logger.log(SentryLevel.DEBUG, "Profiler is already running."); + return; + } + break; + } + if (!isRunning()) { logger.log(SentryLevel.DEBUG, "Started Profiler."); start(); @@ -123,8 +147,7 @@ public void startProfiler( } } - @SuppressWarnings("ReferenceEquality") - private void start() { + private void initScopes() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.forkedRootScopes("profiler"); @@ -133,13 +156,14 @@ private void start() { rateLimiter.addRateLimitObserver(this); } } + } + + @SuppressWarnings("ReferenceEquality") + private void start() { + initScopes(); // Let's initialize trace folder and profiling interval init(); - // init() didn't create profiler, should never happen - if (profiler == null) { - return; - } if (scopes != null) { final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); @@ -152,6 +176,7 @@ 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."); @@ -166,7 +191,13 @@ private void start() { filename = SentryUUID.generateSentryId() + ".jfr"; final String startData; try { - startData = profiler.execute("start,jfr,event=wall,file=" + filename); + // final String command = + // String.format("start,jfr,event=cpu,wall=%s,file=%s",profilingIntervalMicros, filename); + final String command = + String.format( + "start,jfr,event=wall,interval=%s,file=%s", profilingIntervalMicros, filename); + System.out.println(command); + startData = profiler.execute(command); } catch (IOException e) { throw new RuntimeException(e); } @@ -177,7 +208,7 @@ private void start() { isRunning = true; - if (SentryId.EMPTY_ID.equals(profilerId)) { + if (profilerId == SentryId.EMPTY_ID) { profilerId = new SentryId(); } @@ -199,7 +230,24 @@ private void start() { @Override public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - shouldStop = true; + switch (profileLifecycle) { + case TRACE: + rootSpanCounter--; + // If there are active spans, and profile lifecycle is trace, we don't stop the profiler + if (rootSpanCounter > 0) { + return; + } + // rootSpanCounter should never be negative, unless the user changed profile lifecycle + // while the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + shouldStop = true; + break; + case MANUAL: + shouldStop = true; + break; + } } } @@ -209,7 +257,7 @@ private void stop(final boolean restartProfiler) { stopFuture.cancel(true); } // check if profiler was created and it's running - if (profiler == null || !isRunning) { + if (!isRunning) { // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the // ids profilerId = SentryId.EMPTY_ID; @@ -333,11 +381,11 @@ public int getRootSpanCounter() { @Override public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { // We stop the profiler as soon as we are rate limited, to avoid the performance overhead - // if (rateLimiter.isActiveForCategory(All) - // || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { - // logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); - // stop(false); - // } + if (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + stop(false); + } // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's // useless to restart it automatically } From fba188137b4259c63d94f6330065b0eceb5c4866 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 12:07:01 +0200 Subject: [PATCH 13/31] cleanup unused classes from vendor, cleanup packages --- .../api/sentry-async-profiler.api | 222 ++++------ ...AsyncProfilerToSentryProfileConverter.java | 30 +- .../profiling/JavaContinuousProfiler.java | 2 +- ...yncProfilerContinuousProfilerProvider.java | 4 +- ...AsyncProfilerProfileConverterProvider.java | 4 +- .../vendor/asyncprofiler/LICENSE | 201 +++++++++ .../asyncprofiler}/convert/Arguments.java | 2 +- .../asyncprofiler}/convert/Classifier.java | 6 +- .../vendor/asyncprofiler}/convert/Frame.java | 2 +- .../asyncprofiler}/convert/JfrConverter.java | 31 +- .../vendor/asyncprofiler}/jfr/ClassRef.java | 2 +- .../vendor/asyncprofiler}/jfr/Dictionary.java | 2 +- .../asyncprofiler}/jfr/DictionaryInt.java | 2 +- .../vendor/asyncprofiler}/jfr/Element.java | 2 +- .../vendor/asyncprofiler}/jfr/JfrClass.java | 2 +- .../vendor/asyncprofiler}/jfr/JfrField.java | 2 +- .../vendor/asyncprofiler}/jfr/JfrReader.java | 14 +- .../vendor/asyncprofiler}/jfr/MethodRef.java | 2 +- .../vendor/asyncprofiler}/jfr/StackTrace.java | 2 +- .../jfr/event/AllocationSample.java | 2 +- .../asyncprofiler}/jfr/event/CPULoad.java | 4 +- .../jfr/event/ContendedLock.java | 2 +- .../asyncprofiler}/jfr/event/Event.java | 2 +- .../jfr/event/EventAggregator.java | 2 +- .../jfr/event/EventCollector.java | 2 +- .../jfr/event/ExecutionSample.java | 2 +- .../jfr/event/GCHeapSummary.java | 4 +- .../asyncprofiler}/jfr/event/LiveObject.java | 2 +- .../asyncprofiler}/jfr/event/MallocEvent.java | 2 +- .../jfr/event/MallocLeakAggregator.java | 2 +- .../asyncprofiler}/jfr/event/ObjectCount.java | 4 +- .../protocol/jfr/convert/CallStack.java | 32 -- .../protocol/jfr/convert/FlameGraph.java | 408 ------------------ .../io/sentry/protocol/jfr/convert/Index.java | 48 --- .../protocol/jfr/convert/JfrToFlame.java | 94 ---- .../jfr/convert/ResourceProcessor.java | 37 -- ...y.profiling.JavaContinuousProfilerProvider | 2 +- ...try.profiling.JavaProfileConverterProvider | 2 +- 38 files changed, 368 insertions(+), 819 deletions(-) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler}/convert/JfrAsyncProfilerToSentryProfileConverter.java (84%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol => asyncprofiler}/profiling/JavaContinuousProfiler.java (99%) rename sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/{ => provider}/AsyncProfilerContinuousProfilerProvider.java (90%) rename sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/{ => provider}/AsyncProfilerProfileConverterProvider.java (89%) create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/LICENSE rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/convert/Arguments.java (98%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/convert/Classifier.java (96%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/convert/Frame.java (96%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/convert/JfrConverter.java (87%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/ClassRef.java (77%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/Dictionary.java (97%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/DictionaryInt.java (97%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/Element.java (81%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/JfrClass.java (94%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/JfrField.java (90%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/JfrReader.java (96%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/MethodRef.java (84%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/StackTrace.java (86%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/AllocationSample.java (94%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/CPULoad.java (76%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/ContendedLock.java (92%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/Event.java (95%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/EventAggregator.java (98%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/EventCollector.java (86%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/ExecutionSample.java (89%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/GCHeapSummary.java (83%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/LiveObject.java (93%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/MallocEvent.java (86%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/MallocLeakAggregator.java (95%) rename sentry-async-profiler/src/main/java/io/sentry/{protocol/jfr => asyncprofiler/vendor/asyncprofiler}/jfr/event/ObjectCount.java (78%) delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index f5f43b794b4..74bc5a4654f 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -1,19 +1,36 @@ -public final class io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider : io/sentry/profiling/JavaContinuousProfilerProvider { +public final class io/sentry/asyncprofiler/BuildConfig { + public static final field SENTRY_ASYNC_PROFILER_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +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 static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; +} + +public final class io/sentry/asyncprofiler/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { + public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V + public fun close (Z)V + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRootSpanCounter ()I + public fun isRunning ()Z + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V + public fun reevaluateSampling ()V + public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfiler (Lio/sentry/ProfileLifecycle;)V +} + +public final class io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider : io/sentry/profiling/JavaContinuousProfilerProvider { public fun ()V public fun getContinuousProfiler (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)Lio/sentry/IContinuousProfiler; } -public final class io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider : io/sentry/profiling/JavaProfileConverterProvider { +public final class io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider : io/sentry/profiling/JavaProfileConverterProvider { public fun ()V public fun getProfileConverter ()Lio/sentry/IProfileConverter; } -public final class io/sentry/asyncprofiler/BuildConfig { - public static final field SENTRY_ASYNC_PROFILER_SDK_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; -} - -public final class io/sentry/protocol/jfr/convert/Arguments { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments { public field alloc Z public field bci Z public field classify Z @@ -47,25 +64,7 @@ public final class io/sentry/protocol/jfr/convert/Arguments { public fun ([Ljava/lang/String;)V } -public final class io/sentry/protocol/jfr/convert/CallStack { - public fun ()V - public fun clear ()V - public fun pop ()V - public fun push (Ljava/lang/String;B)V -} - -public final class io/sentry/protocol/jfr/convert/FlameGraph : java/util/Comparator { - public fun (Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun addSample (Lio/sentry/protocol/jfr/convert/CallStack;J)V - public fun compare (Lio/sentry/protocol/jfr/convert/Frame;Lio/sentry/protocol/jfr/convert/Frame;)I - public synthetic fun compare (Ljava/lang/Object;Ljava/lang/Object;)I - public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun dump (Ljava/io/PrintStream;)V - public fun parseCollapsed (Ljava/io/Reader;)V - public fun parseHtml (Ljava/io/Reader;)V -} - -public final class io/sentry/protocol/jfr/convert/Frame : java/util/HashMap { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame : java/util/HashMap { public static final field TYPE_C1_COMPILED B public static final field TYPE_CPP B public static final field TYPE_INLINED B @@ -75,33 +74,20 @@ public final class io/sentry/protocol/jfr/convert/Frame : java/util/HashMap { public static final field TYPE_NATIVE B } -public final class io/sentry/protocol/jfr/convert/Index : java/util/HashMap { - public fun (Ljava/lang/Class;Ljava/lang/Object;)V - public fun (Ljava/lang/Class;Ljava/lang/Object;I)V - public fun index (Ljava/lang/Object;)I - public fun keys ()[Ljava/lang/Object; - public fun keys ([Ljava/lang/Object;)V -} - -public final class io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/protocol/jfr/convert/JfrConverter { - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V - public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; - public static fun main ([Ljava/lang/String;)V -} - -public abstract class io/sentry/protocol/jfr/convert/JfrConverter { - protected final field args Lio/sentry/protocol/jfr/convert/Arguments; - protected final field collector Lio/sentry/protocol/jfr/jfr/event/EventCollector; - protected final field jfr Lio/sentry/protocol/jfr/jfr/JfrReader; - protected field methodNames Lio/sentry/protocol/jfr/jfr/Dictionary; - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V +public abstract class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter { + protected final field args Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments; + protected final field collector Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector; + protected final field jfr Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader; + protected field methodNames Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;)V protected fun collectEvents ()V public fun convert ()V protected fun convertChunk ()V - protected fun createCollector (Lio/sentry/protocol/jfr/convert/Arguments;)Lio/sentry/protocol/jfr/jfr/event/EventCollector; - public synthetic fun getCategory (Lio/sentry/protocol/jfr/jfr/StackTrace;)Lio/sentry/protocol/jfr/convert/Classifier$Category; + protected fun createCollector (Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;)Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector; + public synthetic fun getCategory (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace;)Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier$Category; public fun getClassName (J)Ljava/lang/String; public fun getMethodName (JB)Ljava/lang/String; + public fun getPlainThreadName (I)Ljava/lang/String; public fun getStackTraceElement (JBI)Ljava/lang/StackTraceElement; public fun getThreadName (I)Ljava/lang/String; protected fun getThreadStates (Z)Ljava/util/BitSet; @@ -110,85 +96,73 @@ public abstract class io/sentry/protocol/jfr/convert/JfrConverter { protected fun toTicks (J)J } -protected abstract class io/sentry/protocol/jfr/convert/JfrConverter$AggregatedEventVisitor : io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { - protected fun (Lio/sentry/protocol/jfr/convert/JfrConverter;)V - protected abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;J)V - public final fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V -} - -public final class io/sentry/protocol/jfr/convert/JfrToFlame : io/sentry/protocol/jfr/convert/JfrConverter { - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;Lio/sentry/protocol/jfr/convert/Arguments;)V - public static fun convert (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/jfr/convert/Arguments;)V - public fun dump (Ljava/io/OutputStream;)V +protected abstract class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter$AggregatedEventVisitor : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor { + protected fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter;)V + protected abstract fun visit (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;J)V + public final fun visit (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;JJ)V } -public final class io/sentry/protocol/jfr/convert/ResourceProcessor { - public fun ()V - public static fun getResource (Ljava/lang/String;)Ljava/lang/String; - public static fun printTill (Ljava/io/PrintStream;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; -} - -public final class io/sentry/protocol/jfr/jfr/ClassRef { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef { public final field name J public fun (J)V } -public final class io/sentry/protocol/jfr/jfr/Dictionary { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary { public fun ()V public fun (I)V public fun clear ()V - public fun forEach (Lio/sentry/protocol/jfr/jfr/Dictionary$Visitor;)V + public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary$Visitor;)V public fun get (J)Ljava/lang/Object; public fun preallocate (I)I public fun put (JLjava/lang/Object;)V public fun size ()I } -public abstract interface class io/sentry/protocol/jfr/jfr/Dictionary$Visitor { +public abstract interface class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary$Visitor { public abstract fun visit (JLjava/lang/Object;)V } -public final class io/sentry/protocol/jfr/jfr/DictionaryInt { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt { public fun ()V public fun (I)V public fun clear ()V - public fun forEach (Lio/sentry/protocol/jfr/jfr/DictionaryInt$Visitor;)V + public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt$Visitor;)V public fun get (J)I public fun get (JI)I public fun preallocate (I)I public fun put (JI)V } -public abstract interface class io/sentry/protocol/jfr/jfr/DictionaryInt$Visitor { +public abstract interface class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt$Visitor { public abstract fun visit (JI)V } -public final class io/sentry/protocol/jfr/jfr/JfrClass { - public fun field (Ljava/lang/String;)Lio/sentry/protocol/jfr/jfr/JfrField; +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass { + public fun field (Ljava/lang/String;)Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField; } -public final class io/sentry/protocol/jfr/jfr/JfrField { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField { } -public final class io/sentry/protocol/jfr/jfr/JfrReader : java/io/Closeable { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader : java/io/Closeable { public field chunkEndNanos J public field chunkStartNanos J public field chunkStartTicks J - public final field classes Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field classes Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; public field endNanos J public final field enums Ljava/util/Map; - public final field javaThreads Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field methods Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field javaThreads Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; + public final field methods Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; public final field settings Ljava/util/Map; - public final field stackTraces Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field stackTraces Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; public field startNanos J public field startTicks J public field stopAtNewChunk Z - public final field strings Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field symbols Lio/sentry/protocol/jfr/jfr/Dictionary; - public final field threads Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field strings Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; + public final field symbols Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; + public final field threads Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; public field ticksPerSec J - public final field types Lio/sentry/protocol/jfr/jfr/Dictionary; + public final field types Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary; public final field typesByName Ljava/util/Map; public fun (Ljava/lang/String;)V public fun (Ljava/nio/ByteBuffer;)V @@ -207,93 +181,93 @@ public final class io/sentry/protocol/jfr/jfr/JfrReader : java/io/Closeable { public fun incomplete ()Z public fun readAllEvents ()Ljava/util/List; public fun readAllEvents (Ljava/lang/Class;)Ljava/util/List; - public fun readEvent ()Lio/sentry/protocol/jfr/jfr/event/Event; - public fun readEvent (Ljava/lang/Class;)Lio/sentry/protocol/jfr/jfr/event/Event; + public fun readEvent ()Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event; + public fun readEvent (Ljava/lang/Class;)Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event; public fun registerEvent (Ljava/lang/String;Ljava/lang/Class;)V } -public final class io/sentry/protocol/jfr/jfr/MethodRef { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef { public final field cls J public final field name J public final field sig J public fun (JJJ)V } -public final class io/sentry/protocol/jfr/jfr/StackTrace { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace { public final field locations [I public final field methods [J public final field types [B public fun ([J[B[I)V } -public final class io/sentry/protocol/jfr/jfr/event/AllocationSample : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field allocationSize J public final field classId I public final field tlabSize J public fun (JIIIJJ)V public fun classId ()J public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun sameGroup (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)Z public fun value ()J } -public final class io/sentry/protocol/jfr/jfr/event/CPULoad : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field jvmSystem F public final field jvmUser F public final field machineTotal F - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;)V } -public final class io/sentry/protocol/jfr/jfr/event/ContendedLock : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field classId I public final field duration J public fun (JIIJI)V public fun classId ()J public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun sameGroup (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)Z public fun value ()J } -public abstract class io/sentry/protocol/jfr/jfr/event/Event : java/lang/Comparable { +public abstract class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event : java/lang/Comparable { public final field stackTraceId I public final field tid I public final field time J protected fun (JII)V public fun classId ()J - public fun compareTo (Lio/sentry/protocol/jfr/jfr/event/Event;)I + public fun compareTo (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)I public synthetic fun compareTo (Ljava/lang/Object;)I public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun sameGroup (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)Z public fun samples ()J public fun toString ()Ljava/lang/String; public fun value ()J } -public final class io/sentry/protocol/jfr/jfr/event/EventAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector { public fun (ZD)V public fun afterChunk ()V public fun beforeChunk ()V public fun coarsen (D)V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V + public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V + public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;JJ)V public fun finish ()Z - public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V + public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V public fun size ()I } -public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector { +public abstract interface class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector { public abstract fun afterChunk ()V public abstract fun beforeChunk ()V - public abstract fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public abstract fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V public abstract fun finish ()Z - public abstract fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V + public abstract fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V } -public abstract interface class io/sentry/protocol/jfr/jfr/event/EventCollector$Visitor { - public abstract fun visit (Lio/sentry/protocol/jfr/jfr/event/Event;JJ)V +public abstract interface class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor { + public abstract fun visit (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;JJ)V } -public final class io/sentry/protocol/jfr/jfr/event/ExecutionSample : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field samples I public final field threadState I public fun (JIIII)V @@ -301,59 +275,47 @@ public final class io/sentry/protocol/jfr/jfr/event/ExecutionSample : io/sentry/ public fun value ()J } -public final class io/sentry/protocol/jfr/jfr/event/GCHeapSummary : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field afterGC Z public final field committed J public final field gcId I public final field reserved J public final field used J - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;)V } -public final class io/sentry/protocol/jfr/jfr/event/LiveObject : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field allocationSize J public final field allocationTime J public final field classId I public fun (JIIIJJ)V public fun classId ()J public fun hashCode ()I - public fun sameGroup (Lio/sentry/protocol/jfr/jfr/event/Event;)Z + public fun sameGroup (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)Z public fun value ()J } -public final class io/sentry/protocol/jfr/jfr/event/MallocEvent : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field address J public final field size J public fun (JIIJJ)V public fun value ()J } -public final class io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator : io/sentry/protocol/jfr/jfr/event/EventCollector { - public fun (Lio/sentry/protocol/jfr/jfr/event/EventCollector;)V +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector { + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector;)V public fun afterChunk ()V public fun beforeChunk ()V - public fun collect (Lio/sentry/protocol/jfr/jfr/event/Event;)V + public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V public fun finish ()Z - public fun forEach (Lio/sentry/protocol/jfr/jfr/event/EventCollector$Visitor;)V + public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V } -public final class io/sentry/protocol/jfr/jfr/event/ObjectCount : io/sentry/protocol/jfr/jfr/event/Event { +public final class io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event { public final field classId I public final field count J public final field gcId I public final field totalSize J - public fun (Lio/sentry/protocol/jfr/jfr/JfrReader;)V -} - -public final class io/sentry/protocol/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { - public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V - public fun close (Z)V - public fun getProfilerId ()Lio/sentry/protocol/SentryId; - public fun getRootSpanCounter ()I - public fun isRunning ()Z - public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V - public fun reevaluateSampling ()V - public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V - public fun stopProfiler (Lio/sentry/ProfileLifecycle;)V + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;)V } diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java similarity index 84% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java index 9f0c807dc87..f22eb76f709 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -1,17 +1,18 @@ -package io.sentry.protocol.jfr.convert; +package io.sentry.asyncprofiler.convert; import io.sentry.Sentry; import io.sentry.SentryStackTraceFactory; +import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments; +import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.JfrConverter; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; import io.sentry.protocol.SentryStackFrame; -import io.sentry.protocol.jfr.jfr.JfrReader; -import io.sentry.protocol.jfr.jfr.StackTrace; -import io.sentry.protocol.jfr.jfr.event.Event; import io.sentry.protocol.profiling.JfrSample; import io.sentry.protocol.profiling.SentryProfile; import io.sentry.protocol.profiling.ThreadMetadata; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; @@ -25,17 +26,6 @@ public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { super(jfr, args); } - public static void main(String[] args) throws IOException { - - Path jfrPath = - Paths.get( - "/Users/lukasbloder/development/projects/sentry/sentry-java/ff3cb6b172fc45c4ae16d65fb1fc83fe.jfr"); - SentryProfile profile = JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(jfrPath); - // JfrProfile profile2 = new JfrToSentryProfileConverter().convert(jfrPath); - System.out.println(profile.frames); - System.out.println("Done"); - } - @Override protected void convertChunk() { final List events = new ArrayList(); @@ -65,12 +55,7 @@ public void visit(Event event, long value) { jfr.threads.get(event.tid) != null ? jfr.javaThreads.get(event.tid) : event.tid; if (sentryProfile.threadMetadata != null) { - final String threadName = getThreadName(event.tid); - // if(threadName.startsWith("AsyncProfiler-")) { - // // AsyncProfiler threads are not useful for profiling, so we - // skip them - // return; - // } + final String threadName = getPlainThreadName(event.tid); sentryProfile.threadMetadata.computeIfAbsent( String.valueOf(threadIdToUse), k -> { @@ -146,7 +131,6 @@ public void visit(Event event, long value) { instant.getEpochSecond() + instant.getNano() / 1_000_000_000.0; sample.timestamp = timestampDouble; - // sample.threadId = String.valueOf(event.tid); sample.threadId = String.valueOf( jfr.threads.get(event.tid) != null diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java similarity index 99% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java index 847ed8830b5..c182c96ba2b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/profiling/JavaContinuousProfiler.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java @@ -1,4 +1,4 @@ -package io.sentry.protocol.profiling; +package io.sentry.asyncprofiler.profiling; import static io.sentry.DataCategory.All; import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java similarity index 90% rename from sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java index abd112537e4..e721260545b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerContinuousProfilerProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java @@ -1,11 +1,11 @@ -package io.sentry.asyncprofiler; +package io.sentry.asyncprofiler.provider; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler; import io.sentry.profiling.JavaContinuousProfilerProvider; import io.sentry.profiling.JavaProfileConverterProvider; -import io.sentry.protocol.profiling.JavaContinuousProfiler; import org.jetbrains.annotations.NotNull; /** diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java similarity index 89% rename from sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java index 869dd477388..f3db84c55f1 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/AsyncProfilerProfileConverterProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java @@ -1,8 +1,8 @@ -package io.sentry.asyncprofiler; +package io.sentry.asyncprofiler.provider; import io.sentry.IProfileConverter; +import io.sentry.asyncprofiler.convert.JfrAsyncProfilerToSentryProfileConverter; import io.sentry.profiling.JavaProfileConverterProvider; -import io.sentry.protocol.jfr.convert.JfrAsyncProfilerToSentryProfileConverter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/LICENSE b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/LICENSE new file mode 100644 index 00000000000..8dada3edaf5 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java similarity index 98% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java index 8d34033ee6e..d4d81600481 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Arguments.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.convert; +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java similarity index 96% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java index ba33655c58e..7990e1b3e72 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Classifier.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.convert; +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; -import static io.sentry.protocol.jfr.convert.Frame.*; +import static io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Frame.*; -import io.sentry.protocol.jfr.jfr.StackTrace; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Frame.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java similarity index 96% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Frame.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java index c5c64e6341b..d1426255458 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Frame.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.convert; +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; import java.util.HashMap; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java similarity index 87% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java index e670b46bb52..70f1747fac7 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java @@ -3,15 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.convert; - -import static io.sentry.protocol.jfr.convert.Frame.*; - -import io.sentry.protocol.jfr.jfr.ClassRef; -import io.sentry.protocol.jfr.jfr.Dictionary; -import io.sentry.protocol.jfr.jfr.JfrReader; -import io.sentry.protocol.jfr.jfr.MethodRef; -import io.sentry.protocol.jfr.jfr.event.*; +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; + +import static io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Frame.*; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.ClassRef; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.Dictionary; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.MethodRef; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.AllocationSample; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.ContendedLock; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventAggregator; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.ExecutionSample; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.LiveObject; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.MallocEvent; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.MallocLeakAggregator; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.BitSet; @@ -261,6 +269,11 @@ public String getThreadName(int tid) { : threadName.startsWith("[tid=") ? threadName : '[' + threadName + " tid=" + tid + ']'; } + public String getPlainThreadName(int tid) { + String threadName = jfr.threads.get(tid); + return threadName == null ? "[tid=" + tid + ']' : threadName; + } + protected boolean isNativeFrame(byte methodType) { // In JDK Flight Recorder, TYPE_NATIVE denotes Java native methods, // while in async-profiler, TYPE_NATIVE is for C methods diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java similarity index 77% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java index 78e0fbfb577..7c7fc5d8bf0 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/ClassRef.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; public final class ClassRef { public final long name; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java similarity index 97% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java index 47438e3833e..9c9ab8a873c 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Dictionary.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Arrays; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java similarity index 97% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java index 0543a74218e..f552f1fa819 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/DictionaryInt.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Arrays; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Element.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java similarity index 81% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Element.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java index ac7772222e3..127ce9a6262 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/Element.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; abstract class Element { diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java similarity index 94% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java index 6cbb16259f5..d5971b4802c 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrClass.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.ArrayList; import java.util.List; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java similarity index 90% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java index 3c9dc040700..c71787f8379 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrField.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Map; import org.jetbrains.annotations.NotNull; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java similarity index 96% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java index cc6f73cdf95..abc9a0024b4 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/JfrReader.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java @@ -3,9 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; - -import io.sentry.protocol.jfr.jfr.event.*; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.AllocationSample; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.CPULoad; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.ContendedLock; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.ExecutionSample; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.GCHeapSummary; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.LiveObject; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.MallocEvent; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.ObjectCount; import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Constructor; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java similarity index 84% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java index 4e4f203daf3..bbba06b8c0e 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/MethodRef.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; public final class MethodRef { public final long cls; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java similarity index 86% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java index e3fda8c8a1b..f0d7b9d0905 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/StackTrace.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; public final class StackTrace { public final long[] methods; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java similarity index 94% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java index c852d0f1b80..337cbeef563 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/AllocationSample.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public final class AllocationSample extends Event { public final int classId; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java similarity index 76% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java index d504bf20739..f8632a21bd3 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/CPULoad.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; -import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; public final class CPULoad extends Event { public final float jvmUser; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java similarity index 92% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java index 763edb5133b..c0cc52924a4 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ContendedLock.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public final class ContendedLock extends Event { public final long duration; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java similarity index 95% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java index 6cddf8bc48b..323ffb327b6 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/Event.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import java.lang.reflect.Field; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java similarity index 98% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java index 56bf66ebd86..23c9f7aa29c 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventAggregator.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import org.jetbrains.annotations.NotNull; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java similarity index 86% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java index 4ae81889f6b..ac12de630f6 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/EventCollector.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public interface EventCollector { diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java similarity index 89% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java index 3bf836c7a7e..9bbbea38c72 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ExecutionSample.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public final class ExecutionSample extends Event { public final int threadState; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java similarity index 83% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java index ae72cadf3d5..740fbc82245 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/GCHeapSummary.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; -import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; public final class GCHeapSummary extends Event { public final int gcId; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java similarity index 93% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java index ba33391559d..9fcf776ee6b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/LiveObject.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public final class LiveObject extends Event { public final int classId; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java similarity index 86% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java index a67d2f6fc72..eac63a518d0 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocEvent.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; public final class MallocEvent extends Event { public final long address; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java similarity index 95% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java index 556fa8b9795..cde4919bd3e 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/MallocLeakAggregator.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import java.util.ArrayList; import java.util.HashMap; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java similarity index 78% rename from sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java rename to sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java index a38df40372c..dbec70770df 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/jfr/event/ObjectCount.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.sentry.protocol.jfr.jfr.event; +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; -import io.sentry.protocol.jfr.jfr.JfrReader; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; public final class ObjectCount extends Event { public final int gcId; diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java deleted file mode 100644 index dbd62a192c2..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/CallStack.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import java.util.Arrays; - -public final class CallStack { - String[] names = new String[16]; - byte[] types = new byte[16]; - int size; - - public void push(String name, byte type) { - if (size >= names.length) { - names = Arrays.copyOf(names, size * 2); - types = Arrays.copyOf(types, size * 2); - } - names[size] = name; - types[size] = type; - size++; - } - - public void pop() { - size--; - } - - public void clear() { - size = 0; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java deleted file mode 100644 index 022ce746d96..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/FlameGraph.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import static io.sentry.protocol.jfr.convert.Frame.*; -import static io.sentry.protocol.jfr.convert.ResourceProcessor.*; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Comparator; -import java.util.StringTokenizer; -import java.util.regex.Pattern; -import org.jetbrains.annotations.NotNull; - -public final class FlameGraph implements Comparator { - private static final Frame[] EMPTY_FRAME_ARRAY = {}; - private static final String[] FRAME_SUFFIX = {"_[0]", "_[j]", "_[i]", "", "", "_[k]", "_[1]"}; - private static final byte HAS_SUFFIX = (byte) 0x80; - private static final int FLUSH_THRESHOLD = 15000; - - private final Arguments args; - private final Index cpool = new Index<>(String.class, ""); - private final Frame root = new Frame(0, TYPE_NATIVE); - private final StringBuilder outbuf = new StringBuilder(FLUSH_THRESHOLD + 1000); - private @NotNull int[] order; - private int depth; - private int lastLevel; - private long lastX; - private long lastTotal; - private long mintotal; - - public FlameGraph(@NotNull Arguments args) { - this.args = args; - this.order = new int[0]; // Initialize with empty array - } - - public void parseCollapsed(Reader in) throws IOException { - CallStack stack = new CallStack(); - - try (BufferedReader br = new BufferedReader(in)) { - for (String line; (line = br.readLine()) != null; ) { - int space = line.lastIndexOf(' '); - if (space <= 0) continue; - - long ticks = Long.parseLong(line.substring(space + 1)); - - for (int from = 0, to; from < space; from = to + 1) { - if ((to = line.indexOf(';', from)) < 0) to = space; - String name = line.substring(from, to); - byte type = detectType(name); - if ((type & HAS_SUFFIX) != 0) { - name = name.substring(0, name.length() - 4); - type ^= HAS_SUFFIX; - } - stack.push(name, type); - } - - addSample(stack, ticks); - stack.clear(); - } - } - } - - public void parseHtml(Reader in) throws IOException { - Frame[] levels = new Frame[128]; - int level = 0; - long total = 0; - boolean needRebuild = args.reverse || args.include != null || args.exclude != null; - - try (BufferedReader br = new BufferedReader(in)) { - while (!br.readLine().startsWith("const cpool")) - ; - br.readLine(); - - String s = ""; - for (String line; (line = br.readLine()).startsWith("'"); ) { - String packed = unescape(line.substring(1, line.lastIndexOf('\''))); - s = s.substring(0, packed.charAt(0) - ' ').concat(packed.substring(1)); - cpool.put(s, cpool.size()); - } - - while (!br.readLine().isEmpty()) - ; - - for (String line; !(line = br.readLine()).isEmpty(); ) { - StringTokenizer st = new StringTokenizer(line.substring(2, line.length() - 1), ","); - int nameAndType = Integer.parseInt(st.nextToken()); - - char func = line.charAt(0); - if (func == 'f') { - level = Integer.parseInt(st.nextToken()); - st.nextToken(); - } else if (func == 'u') { - level++; - } else if (func != 'n') { - throw new IllegalStateException("Unexpected line: " + line); - } - - if (st.hasMoreTokens()) { - total = Long.parseLong(st.nextToken()); - } - - int titleIndex = nameAndType >>> 3; - byte type = (byte) (nameAndType & 7); - if (st.hasMoreTokens() && (type <= TYPE_INLINED || type >= TYPE_C1_COMPILED)) { - type = TYPE_JIT_COMPILED; - } - - Frame f = level > 0 || needRebuild ? new Frame(titleIndex, type) : root; - f.self = f.total = total; - if (st.hasMoreTokens()) f.inlined = Long.parseLong(st.nextToken()); - if (st.hasMoreTokens()) f.c1 = Long.parseLong(st.nextToken()); - if (st.hasMoreTokens()) f.interpreted = Long.parseLong(st.nextToken()); - - if (level > 0) { - Frame parent = levels[level - 1]; - parent.put(f.key, f); - parent.self -= total; - depth = Math.max(depth, level); - } - if (level >= levels.length) { - levels = Arrays.copyOf(levels, level * 2); - } - levels[level] = f; - } - } - - if (needRebuild) { - rebuild(levels[0], new CallStack(), cpool.keys()); - } - } - - private void rebuild(Frame frame, CallStack stack, String[] strings) { - if (frame.self > 0) { - addSample(stack, frame.self); - } - if (!frame.isEmpty()) { - for (Frame child : frame.values()) { - stack.push(strings[child.getTitleIndex()], child.getType()); - rebuild(child, stack, strings); - stack.pop(); - } - } - } - - public void addSample(CallStack stack, long ticks) { - if (excludeStack(stack)) { - return; - } - - Frame frame = root; - if (args.reverse) { - for (int i = stack.size; --i >= args.skip; ) { - frame = addChild(frame, stack.names[i], stack.types[i], ticks); - } - } else { - for (int i = args.skip; i < stack.size; i++) { - frame = addChild(frame, stack.names[i], stack.types[i], ticks); - } - } - frame.total += ticks; - frame.self += ticks; - - depth = Math.max(depth, stack.size); - } - - public void dump(PrintStream out) { - mintotal = (long) (root.total * args.minwidth / 100); - - if ("collapsed".equals(args.output)) { - printFrameCollapsed(out, root, cpool.keys()); - return; - } - - String tail = getResource("/flame.html"); - - tail = printTill(out, tail, "/*height:*/300"); - int depth = mintotal > 1 ? root.depth(mintotal) : this.depth + 1; - out.print(Math.min(depth * 16, 32767)); - - tail = printTill(out, tail, "/*title:*/"); - out.print(args.title); - - // inverted toggles the layout for reversed stacktraces from icicle to flamegraph - // and for default stacktraces from flamegraphs to icicle. - tail = printTill(out, tail, "/*inverted:*/false"); - out.print(args.reverse ^ args.inverted); - - tail = printTill(out, tail, "/*depth:*/0"); - out.print(depth); - - tail = printTill(out, tail, "/*cpool:*/"); - printCpool(out); - - tail = printTill(out, tail, "/*frames:*/"); - printFrame(out, root, 0, 0); - out.print(outbuf); - - tail = printTill(out, tail, "/*highlight:*/"); - out.print(args.highlight != null ? "'" + escape(args.highlight) + "'" : ""); - - out.print(tail); - } - - private void printCpool(PrintStream out) { - String[] strings = cpool.keys(); - Arrays.sort(strings); - out.print("'all'"); - - order = new int[strings.length]; - String s = ""; - for (int i = 1; i < strings.length; i++) { - int prefixLen = Math.min(getCommonPrefix(s, s = strings[i]), 95); - out.print(",\n'" + escape((char) (prefixLen + ' ') + s.substring(prefixLen)) + "'"); - order[cpool.index(s)] = i; - } - - // cpool is not used beyond this point - cpool.clear(); - } - - private void printFrame(PrintStream out, Frame frame, int level, long x) { - int nameAndType = order[frame.getTitleIndex()] << 3 | frame.getType(); - boolean hasExtraTypes = - (frame.inlined | frame.c1 | frame.interpreted) != 0 - && frame.inlined < frame.total - && frame.interpreted < frame.total; - - char func = 'f'; - if (level == lastLevel + 1 && x == lastX) { - func = 'u'; - } else if (level == lastLevel && x == lastX + lastTotal) { - func = 'n'; - } - - StringBuilder sb = outbuf.append(func).append('(').append(nameAndType); - if (func == 'f') { - sb.append(',').append(level).append(',').append(x - lastX); - } - if (frame.total != lastTotal || hasExtraTypes) { - sb.append(',').append(frame.total); - if (hasExtraTypes) { - sb.append(',') - .append(frame.inlined) - .append(',') - .append(frame.c1) - .append(',') - .append(frame.interpreted); - } - } - sb.append(")\n"); - - if (sb.length() > FLUSH_THRESHOLD) { - out.print(sb); - sb.setLength(0); - } - - lastLevel = level; - lastX = x; - lastTotal = frame.total; - - Frame[] children = frame.values().toArray(EMPTY_FRAME_ARRAY); - Arrays.sort(children, this); - - x += frame.self; - for (Frame child : children) { - if (child.total >= mintotal) { - printFrame(out, child, level + 1, x); - } - x += child.total; - } - } - - private void printFrameCollapsed(PrintStream out, Frame frame, String[] strings) { - StringBuilder sb = outbuf; - int prevLength = sb.length(); - - if (!root.equals(frame)) { - sb.append(strings[frame.getTitleIndex()]).append(FRAME_SUFFIX[frame.getType()]); - if (frame.self > 0) { - int tmpLength = sb.length(); - out.print(sb.append(' ').append(frame.self).append('\n')); - sb.setLength(tmpLength); - } - sb.append(';'); - } - - if (!frame.isEmpty()) { - for (Frame child : frame.values()) { - if (child.total >= mintotal) { - printFrameCollapsed(out, child, strings); - } - } - } - - sb.setLength(prevLength); - } - - private boolean excludeStack(CallStack stack) { - Pattern include = args.include; - Pattern exclude = args.exclude; - if (include == null && exclude == null) { - return false; - } - - for (int i = 0; i < stack.size; i++) { - if (exclude != null && exclude.matcher(stack.names[i]).matches()) { - return true; - } - if (include != null && include.matcher(stack.names[i]).matches()) { - if (exclude == null) return false; - include = null; - } - } - - return include != null; - } - - private Frame addChild(Frame frame, String title, byte type, long ticks) { - frame.total += ticks; - - int titleIndex = cpool.index(title); - - Frame child; - switch (type) { - case TYPE_INTERPRETED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).interpreted += ticks; - break; - case TYPE_INLINED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).inlined += ticks; - break; - case TYPE_C1_COMPILED: - (child = frame.getChild(titleIndex, TYPE_JIT_COMPILED)).c1 += ticks; - break; - default: - child = frame.getChild(titleIndex, type); - } - return child; - } - - @SuppressWarnings("OperatorPrecedence") - private static byte detectType(String title) { - if (title.endsWith("_[j]")) { - return TYPE_JIT_COMPILED | HAS_SUFFIX; - } else if (title.endsWith("_[i]")) { - return TYPE_INLINED | HAS_SUFFIX; - } else if (title.endsWith("_[k]")) { - return TYPE_KERNEL | HAS_SUFFIX; - } else if (title.endsWith("_[0]")) { - return TYPE_INTERPRETED | HAS_SUFFIX; - } else if (title.endsWith("_[1]")) { - return TYPE_C1_COMPILED | HAS_SUFFIX; - } else if (title.contains("::") || title.startsWith("-[") || title.startsWith("+[")) { - return TYPE_CPP; - } else if (title.indexOf('/') > 0 && title.charAt(0) != '[' - || title.indexOf('.') > 0 && Character.isUpperCase(title.charAt(0))) { - return TYPE_JIT_COMPILED; - } else { - return TYPE_NATIVE; - } - } - - private static int getCommonPrefix(String a, String b) { - int length = Math.min(a.length(), b.length()); - for (int i = 0; i < length; i++) { - if (a.charAt(i) != b.charAt(i) || a.charAt(i) > 127) { - return i; - } - } - return length; - } - - private static String escape(String s) { - if (s.indexOf('\\') >= 0) s = s.replace("\\", "\\\\"); - if (s.indexOf('\'') >= 0) s = s.replace("'", "\\'"); - return s; - } - - private static String unescape(String s) { - if (s.indexOf('\'') >= 0) s = s.replace("\\'", "'"); - if (s.indexOf('\\') >= 0) s = s.replace("\\\\", "\\"); - return s; - } - - @Override - public int compare(Frame f1, Frame f2) { - return order[f1.getTitleIndex()] - order[f2.getTitleIndex()]; - } - - public static void convert(String input, String output, Arguments args) throws IOException { - FlameGraph fg = new FlameGraph(args); - try (InputStreamReader in = - new InputStreamReader(new FileInputStream(input), StandardCharsets.UTF_8)) { - if (input.endsWith(".html")) { - fg.parseHtml(in); - } else { - fg.parseCollapsed(in); - } - } - try (PrintStream out = new PrintStream(output, "UTF-8")) { - fg.dump(out); - } - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java deleted file mode 100644 index 65b66bf6012..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/Index.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import java.lang.reflect.Array; -import java.util.HashMap; - -public final class Index extends HashMap { - private static final long serialVersionUID = 1L; - private final Class cls; - - public Index(Class cls, T empty) { - this(cls, empty, 256); - } - - public Index(Class cls, T empty, int initialCapacity) { - super(initialCapacity); - this.cls = cls; - super.put(empty, 0); - } - - public int index(T key) { - Integer index = super.get(key); - if (index != null) { - return index; - } else { - int newIndex = super.size(); - super.put(key, newIndex); - return newIndex; - } - } - - @SuppressWarnings("unchecked") - public T[] keys() { - T[] result = (T[]) Array.newInstance(cls, size()); - keys(result); - return result; - } - - public void keys(T[] result) { - for (Entry entry : entrySet()) { - result[entry.getValue()] = entry.getKey(); - } - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java deleted file mode 100644 index d8f13e746d3..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/JfrToFlame.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import static io.sentry.protocol.jfr.convert.Frame.*; - -import io.sentry.protocol.jfr.jfr.JfrReader; -import io.sentry.protocol.jfr.jfr.StackTrace; -import io.sentry.protocol.jfr.jfr.event.AllocationSample; -import io.sentry.protocol.jfr.jfr.event.Event; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; - -/** Converts .jfr output to HTML Flame Graph. */ -public final class JfrToFlame extends JfrConverter { - private final FlameGraph fg; - - public JfrToFlame(JfrReader jfr, Arguments args) { - super(jfr, args); - this.fg = new FlameGraph(args); - } - - @Override - protected void convertChunk() { - collector.forEach( - new AggregatedEventVisitor() { - final CallStack stack = new CallStack(); - - @Override - public void visit(Event event, long value) { - StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); - if (stackTrace != null) { - Arguments args = JfrToFlame.this.args; - long[] methods = stackTrace.methods; - byte[] types = stackTrace.types; - int[] locations = stackTrace.locations; - - if (args.threads) { - stack.push(getThreadName(event.tid), TYPE_NATIVE); - } - if (args.classify) { - Classifier.Category category = getCategory(stackTrace); - if (category != null) { - stack.push(category.title, category.type); - } - } - for (int i = methods.length; --i >= 0; ) { - String methodName = getMethodName(methods[i], types[i]); - int location; - if (args.lines && (location = locations[i] >>> 16) != 0) { - methodName += ":" + location; - } else if (args.bci && (location = locations[i] & 0xffff) != 0) { - methodName += "@" + location; - } - stack.push(methodName, types[i]); - } - long classId = event.classId(); - if (classId != 0) { - stack.push( - getClassName(classId), - (event instanceof AllocationSample) && ((AllocationSample) event).tlabSize == 0 - ? TYPE_KERNEL - : TYPE_INLINED); - } - - fg.addSample(stack, value); - stack.clear(); - } - } - }); - } - - public void dump(OutputStream out) throws IOException { - try (PrintStream ps = new PrintStream(out, false, "UTF-8")) { - fg.dump(ps); - } - } - - public static void convert(String input, String output, Arguments args) throws IOException { - JfrToFlame converter; - try (JfrReader jfr = new JfrReader(input)) { - converter = new JfrToFlame(jfr, args); - converter.convert(); - } - try (FileOutputStream out = new FileOutputStream(output)) { - converter.dump(out); - } - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java b/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java deleted file mode 100644 index 7e061ded7a1..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/protocol/jfr/convert/ResourceProcessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.protocol.jfr.convert; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; - -public final class ResourceProcessor { - - public static String getResource(String name) { - try (InputStream stream = ResourceProcessor.class.getResourceAsStream(name)) { - if (stream == null) { - throw new IOException("No resource found"); - } - - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[32768]; - for (int length; (length = stream.read(buffer)) != -1; ) { - result.write(buffer, 0, length); - } - return result.toString("UTF-8"); - } catch (IOException e) { - throw new IllegalStateException("Can't load resource with name " + name); - } - } - - public static String printTill(PrintStream out, String data, String till) { - int index = data.indexOf(till); - out.print(data.substring(0, index)); - return data.substring(index + till.length()); - } -} diff --git a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider index 7d792ae8ff2..a59cb70f73c 100644 --- a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider +++ b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider @@ -1 +1 @@ -io.sentry.asyncprofiler.AsyncProfilerContinuousProfilerProvider \ No newline at end of file +io.sentry.asyncprofiler.provider.AsyncProfilerContinuousProfilerProvider diff --git a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider index bd97c6688d6..5f39755545d 100644 --- a/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider +++ b/sentry-async-profiler/src/main/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider @@ -1 +1 @@ -io.sentry.asyncprofiler.AsyncProfilerProfileConverterProvider \ No newline at end of file +io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider From 55df61c4ad05fab2d1524dbbdfe2eb210a8c58fc Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 21:46:18 +0200 Subject: [PATCH 14/31] allow setting profiling-traces-dir-path independently from cache dir using external options --- sentry/api/sentry.api | 3 +++ .../main/java/io/sentry/ExternalOptions.java | 11 ++++++++++ sentry/src/main/java/io/sentry/Sentry.java | 11 +++++++--- .../main/java/io/sentry/SentryOptions.java | 21 +++++++++++++++++-- .../java/io/sentry/ExternalOptionsTest.kt | 7 +++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 20 ++++++++++++++++++ 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1a2bf18674d..78e0dac4f49 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -491,6 +491,7 @@ public final class io/sentry/ExternalOptions { public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean; public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; + public fun getProfilingTracesDirPath ()Ljava/lang/String; public fun getProguardUuid ()Ljava/lang/String; public fun getProxy ()Lio/sentry/SentryOptions$Proxy; public fun getRelease ()Ljava/lang/String; @@ -533,6 +534,7 @@ public final class io/sentry/ExternalOptions { public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V + public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProguardUuid (Ljava/lang/String;)V public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V public fun setRelease (Ljava/lang/String;)V @@ -3479,6 +3481,7 @@ public class io/sentry/SentryOptions { public fun setProfileSessionSampleRate (Ljava/lang/Double;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V + public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V public fun setProguardUuid (Ljava/lang/String;)V public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 4edeb975846..c2ba950c5ef 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -57,6 +57,7 @@ public final class ExternalOptions { private @Nullable Boolean captureOpenTelemetryEvents; private @Nullable Double profileSessionSampleRate; + private @Nullable String profilingTracesDirPath; private @Nullable SentryOptions.Cron cron; @@ -207,6 +208,8 @@ public final class ExternalOptions { options.setProfileSessionSampleRate( propertiesProvider.getDoubleProperty("profile-session-sample-rate")); + options.setProfilingTracesDirPath(propertiesProvider.getProperty("profiling-traces-dir-path")); + return options; } @@ -543,4 +546,12 @@ public void setEnableLogs(final @Nullable Boolean enableLogs) { public void setProfileSessionSampleRate(@Nullable Double profileSessionSampleRate) { this.profileSessionSampleRate = profileSessionSampleRate; } + + public @Nullable String getProfilingTracesDirPath() { + return profilingTracesDirPath; + } + + public void setProfilingTracesDirPath(@Nullable String profilingTracesDirPath) { + this.profilingTracesDirPath = profilingTracesDirPath; + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 74c9b1548fc..ca58bd79487 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -666,11 +666,16 @@ private static void initConfigurations(final @NotNull SentryOptions options) { options.getBackpressureMonitor().start(); } - // TODO: make this configurable - if (options.isContinuousProfilingEnabled() && profilingTracesDirPath != null) { + if (options.isContinuousProfilingEnabled() + && profilingTracesDirPath != null + && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { final IContinuousProfiler continuousProfiler = ProfilingServiceLoader.loadContinuousProfiler( - new SystemOutLogger(), profilingTracesDirPath, 10, options.getExecutorService()); + new SystemOutLogger(), + profilingTracesDirPath, + options.getProfilingTracesHz(), + options.getExecutorService()); + options.setContinuousProfiler(continuousProfiler); } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index bd829a66e10..a1a7bb3da4f 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -578,6 +578,8 @@ public class SentryOptions { private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); + private @Nullable String profilingTracesDirPath; + /** * Adds an event processor * @@ -2026,14 +2028,25 @@ public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { * @return the profiling traces dir. path or null if not set */ public @Nullable String getProfilingTracesDirPath() { + if (profilingTracesDirPath != null && !profilingTracesDirPath.isEmpty()) { + return dsnHash != null + ? new File(profilingTracesDirPath, dsnHash).getAbsolutePath() + : profilingTracesDirPath; + } + final String cacheDirPath = getCacheDirPath(); + if (cacheDirPath == null) { - // TODO: Should we add ExternalOptions to let users define the tracesDirPath? - return new File(".", "profiling_traces").getAbsolutePath(); + return null; } + return new File(cacheDirPath, "profiling_traces").getAbsolutePath(); } + public void setProfilingTracesDirPath(final @Nullable String profilingTracesDirPath) { + this.profilingTracesDirPath = profilingTracesDirPath; + } + /** * Returns a list of origins to which `sentry-trace` header should be sent in HTTP integrations. * @@ -3223,6 +3236,10 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getProfileSessionSampleRate() != null) { setProfileSessionSampleRate(options.getProfileSessionSampleRate()); } + + if (options.getProfilingTracesDirPath() != null) { + setProfilingTracesDirPath(options.getProfilingTracesDirPath()); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 95b540e8179..9f32d8cb8f8 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -383,6 +383,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with profilingTracesDirPath set to profile_traces`() { + withPropertiesFile("profiling-traces-dir-path=profile_traces") { options -> + assertTrue(options.profilingTracesDirPath == "profile_traces") + } + } + private fun withPropertiesFile( textLines: List = emptyList(), logger: ILogger = mock(), diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 6efc63ebfb3..8890af5a320 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -408,7 +408,9 @@ class SentryOptionsTest { externalOptions.isGlobalHubMode = true externalOptions.isEnableLogs = true externalOptions.profileSessionSampleRate = 0.8 + externalOptions.profilingTracesDirPath = "/profiling-traces" + val hash = StringUtils.calculateStringHash(externalOptions.dsn, mock()) val options = SentryOptions() options.merge(externalOptions) @@ -463,6 +465,7 @@ class SentryOptionsTest { assertTrue(options.isGlobalHubMode!!) assertTrue(options.logs.isEnabled!!) assertEquals(0.8, options.profileSessionSampleRate) + assertEquals("/profiling-traces${File.separator}${hash}", options.profilingTracesDirPath) } @Test @@ -534,6 +537,23 @@ class SentryOptionsTest { ) } + @Test + fun `when cacheDirPath and profilingTracesDirPath are set, profilingTracesDirPath takes precedence`() { + val dsn = "http://key@localhost/proj" + val hash = StringUtils.calculateStringHash(dsn, mock()) + val options = + SentryOptions().apply { + setDsn(dsn) + cacheDirPath = "${File.separator}test" + profilingTracesDirPath = "${File.separator}test-profiles" + } + + assertEquals( + "${File.separator}test-profiles${File.separator}${hash}", + options.profilingTracesDirPath, + ) + } + @Test fun `getCacheDirPathWithoutDsn does not contain dsn hash`() { val dsn = "http://key@localhost/proj" From 9bcd4ea5e8c214ae6a604cf59d3c06fd87e86ac3 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 21:47:31 +0200 Subject: [PATCH 15/31] use profileChunk.platform to decide how to deal with the chunk instead of file extension --- .../java/io/sentry/SentryEnvelopeItem.java | 3 +- .../java/io/sentry/SentryEnvelopeItemTest.kt | 42 +++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 6a3e5e91804..5d236bfe2c7 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 (traceFile.getName().endsWith(".jfr")) { + if (profileChunk.getPlatform().equals("java")) { final IProfileConverter profileConverter = ProfilingServiceLoader.loadProfileConverter(); if (profileConverter != null) { @@ -307,7 +307,6 @@ private static void ensureAttachmentSizeLimit( throw new SentryEnvelopeException("Profile conversion failed"); } } - // If no converter is available, JFR profile conversion is skipped } else { // The payload of the profile item is a json including the trace file encoded with // base64 diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index cd58b5ae345..026177dfc44 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -434,7 +434,11 @@ class SentryEnvelopeItemTest { @Test fun `fromProfilingTrace with unreadable file throws`() { val file = File(fixture.pathname) - val profilingTraceData = mock { whenever(it.traceFile).thenReturn(file) } + val profilingTraceData = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } file.writeBytes(fixture.bytes) file.setReadable(false) assertFailsWith( @@ -492,7 +496,11 @@ class SentryEnvelopeItemTest { @Test fun `fromProfileChunk saves file as Base64`() { val file = File(fixture.pathname) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } file.writeBytes(fixture.bytes) val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data @@ -503,7 +511,11 @@ class SentryEnvelopeItemTest { @Test fun `fromProfileChunk deletes file only after reading data`() { val file = File(fixture.pathname) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } file.writeBytes(fixture.bytes) assert(file.exists()) @@ -516,7 +528,11 @@ class SentryEnvelopeItemTest { @Test fun `fromProfileChunk with invalid file throws`() { val file = File(fixture.pathname) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } assertFailsWith( "Dropping profiling trace data, because the file ${file.path} doesn't exists" @@ -528,7 +544,11 @@ class SentryEnvelopeItemTest { @Test fun `fromProfileChunk with unreadable file throws`() { val file = File(fixture.pathname) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } file.writeBytes(fixture.bytes) file.setReadable(false) assertFailsWith( @@ -542,7 +562,11 @@ class SentryEnvelopeItemTest { fun `fromProfileChunk with empty file throws`() { val file = File(fixture.pathname) file.writeBytes(ByteArray(0)) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) assertFailsWith("Profiling trace file is empty") { chunk.data } @@ -553,7 +577,11 @@ class SentryEnvelopeItemTest { val file = File(fixture.pathname) val maxSize = 50 * 1024 * 1024 // 50MB file.writeBytes(ByteArray((maxSize + 1)) { 0 }) - val profileChunk = mock { whenever(it.traceFile).thenReturn(file) } + val profileChunk = + mock { + whenever(it.traceFile).thenReturn(file) + whenever(it.platform).thenReturn("android") + } val exception = assertFailsWith { From 3f83146ebcc3b93ff8b883ca49f597e23cde6a7e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 21:50:35 +0200 Subject: [PATCH 16/31] port relevant AndroidContinuousProfilerTest tests to JavaContinuousProfilerTest --- .../profiling/JavaContinuousProfiler.java | 31 +- .../JavaContinuousProfilerTest.kt | 433 ++++++++++++++++++ 2 files changed, 449 insertions(+), 15 deletions(-) create mode 100644 sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt 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 c182c96ba2b..dd8db6237bc 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 @@ -56,7 +56,6 @@ public final class JavaContinuousProfiler private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); - private final @NotNull String profilingIntervalMicros; private @NotNull String filename = ""; @@ -79,29 +78,28 @@ public JavaContinuousProfiler( this.profilingTracesHz = profilingTracesHz; this.executorService = executorService; this.profiler = AsyncProfiler.getInstance(); - this.profilingIntervalMicros = - String.format("%dus", (int) SECONDS.toMicros(1) / profilingTracesHz); } - private void init() { + private boolean init() { // We initialize it only once if (isInitialized) { - return; + return true; } isInitialized = true; if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, "Disabling profiling because no profiling traces dir path is defined in options."); - return; + return false; } if (profilingTracesHz <= 0) { logger.log( SentryLevel.WARNING, "Disabling profiling because trace rate is set to %d", profilingTracesHz); - return; + return false; } + return true; } @SuppressWarnings("ReferenceEquality") @@ -150,7 +148,8 @@ public void startProfiler( private void initScopes() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { - this.scopes = Sentry.forkedRootScopes("profiler"); + // TODO: should we fork the scopes here? + this.scopes = Sentry.getCurrentScopes(); final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); if (rateLimiter != null) { rateLimiter.addRateLimitObserver(this); @@ -163,7 +162,9 @@ private void start() { initScopes(); // Let's initialize trace folder and profiling interval - init(); + if (!init()) { + return; + } if (scopes != null) { final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); @@ -188,18 +189,18 @@ private void start() { } else { startProfileChunkTimestamp = new SentryNanotimeDate(); } - filename = SentryUUID.generateSentryId() + ".jfr"; - final String startData; + filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr"; + String startData = null; try { - // final String command = - // String.format("start,jfr,event=cpu,wall=%s,file=%s",profilingIntervalMicros, filename); + 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); System.out.println(command); startData = profiler.execute(command); - } catch (IOException e) { - throw new RuntimeException(e); + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Failed to start profiling: ", e); } // check if profiling started if (startData == null) { diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt new file mode 100644 index 00000000000..3c895fd7b84 --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilerTest.kt @@ -0,0 +1,433 @@ +package io.sentry.asyncprofiler + +import io.sentry.DataCategory +import io.sentry.IConnectionStatusProvider +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.ProfileLifecycle +import io.sentry.Sentry +import io.sentry.SentryLevel +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 +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +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 +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class JavaContinuousProfilerTest { + + private val fixture = Fixture() + + private class Fixture { + private val mockDsn = "http://key@localhost/proj" + val executor = DeferredExecutorService() + val mockedSentry = mockStatic(Sentry::class.java) + val mockLogger = mock() + val mockTracesSampler = mock() + + val scopes: IScopes = mock() + + lateinit var transaction1: SentryTracer + lateinit var transaction2: SentryTracer + lateinit var transaction3: SentryTracer + + 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) = {}): JavaContinuousProfiler { + options.executorService = executor + optionConfig(options) + whenever(scopes.options).thenReturn(options) + transaction1 = SentryTracer(TransactionContext("", ""), scopes) + transaction2 = SentryTracer(TransactionContext("", ""), scopes) + transaction3 = SentryTracer(TransactionContext("", ""), scopes) + return JavaContinuousProfiler( + options.logger, + options.profilingTracesDirPath, + options.profilingTracesHz, + options.executorService, + ) + } + } + + @BeforeTest + fun `set up`() { + // Profiler doesn't start if the folder doesn't exists. + // Usually it's generated when calling Sentry.init, but for tests we can create it manually. + + fixture.options.cacheDirPath = "." + File(fixture.options.profilingTracesDirPath!!).mkdirs() + + Sentry.setCurrentScopes(fixture.scopes) + + fixture.mockedSentry.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + fixture.mockedSentry.`when` { Sentry.close() }.then { fixture.executor.runAll() } + } + + @AfterTest + fun clear() { + fixture.options.profilingTracesDirPath?.let { File(it).deleteRecursively() } + fixture.options.cacheDirPath?.let { File(it).deleteRecursively() } + + Sentry.stopProfiler() + Sentry.close() + fixture.mockedSentry.close() + } + + @Test + fun `isRunning reflects profiler status`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() + assertFalse(profiler.isRunning) + } + + @Test + fun `stopProfiler stops the profiler after chunk is finished`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + // We are scheduling the profiler to stop at the end of the chunk, so it should still be running + profiler.stopProfiler(ProfileLifecycle.MANUAL) + assertTrue(profiler.isRunning) + // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart + fixture.executor.runAll() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler multiple starts are ignored in manual mode`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + verify(fixture.mockLogger, never()) + .log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + assertTrue(profiler.isRunning) + assertEquals(0, profiler.rootSpanCounter) + } + + @Test + fun `profiler multiple starts are accepted in trace mode`() { + val profiler = fixture.getSut() + + // rootSpanCounter is incremented when the profiler starts in trace mode + assertEquals(0, profiler.rootSpanCounter) + profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + verify(fixture.mockLogger, never()) + .log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + assertTrue(profiler.isRunning) + assertEquals(2, profiler.rootSpanCounter) + + // rootSpanCounter is decremented when the profiler stops in trace mode, and keeps running until + // rootSpanCounter is 0 + profiler.stopProfiler(ProfileLifecycle.TRACE) + fixture.executor.runAll() + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + + // only when rootSpanCounter is 0 the profiler stops + profiler.stopProfiler(ProfileLifecycle.TRACE) + fixture.executor.runAll() + assertEquals(0, profiler.rootSpanCounter) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler logs a warning on start if not sampled`() { + val profiler = fixture.getSut() + whenever(fixture.mockTracesSampler.sampleSessionProfile(any())).thenReturn(false) + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + verify(fixture.mockLogger) + .log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) + } + + @Test + fun `profiler evaluates sessionSampleRate only the first time`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // Then, the sessionSampleRate is not evaluated again + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + } + + @Test + fun `when reevaluateSampling, profiler evaluates sessionSampleRate on next start`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // When reevaluateSampling is called, the sessionSampleRate is not evaluated immediately + profiler.reevaluateSampling() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // Then, when the profiler starts again, the sessionSampleRate is reevaluated + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile(any()) + } + + @Test + fun `profiler ignores profilesSampleRate`() { + val profiler = fixture.getSut { it.profilesSampleRate = 0.0 } + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + profiler.close(true) + } + + @Test + fun `profiler evaluates profilingTracesDirPath options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { it.cacheDirPath = null } + verify(fixture.mockLogger, never()) + .log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options.", + ) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only + // once + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger, times(1)) + .log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options.", + ) + } + + @Test + fun `profiler evaluates profilingTracesHz options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { it.profilingTracesHz = 0 } + verify(fixture.mockLogger, never()) + .log(SentryLevel.WARNING, "Disabling profiling because trace rate is set to %d", 0) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only + // once + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger, times(1)) + .log(SentryLevel.WARNING, "Disabling profiling because trace rate is set to %d", 0) + } + + @Test + fun `profiler on tracesDirPath null`() { + val profiler = fixture.getSut { it.cacheDirPath = null } + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on tracesDirPath empty`() { + val profiler = fixture.getSut { it.cacheDirPath = "" } + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on profilingTracesHz 0`() { + val profiler = fixture.getSut { it.profilingTracesHz = 0 } + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler does not throw if traces cannot be written to disk`() { + val profiler = fixture.getSut { File(it.profilingTracesDirPath!!).setWritable(false) } + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() + // We assert that no trace files are written + assertTrue(File(fixture.options.profilingTracesDirPath!!).list()!!.isEmpty()) + 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() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + fixture.executor.runAll() + verify(fixture.mockLogger) + .log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) + assertTrue(profiler.isRunning) + + fixture.executor.runAll() + verify(fixture.mockLogger, times(2)) + .log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) + assertTrue(profiler.isRunning) + } + + @Test + fun `profiler sends chunk on each restart`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + fixture.executor.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + // Now the executor is used to send the chunk + fixture.executor.runAll() + verify(fixture.scopes).captureProfileChunk(any()) + } + + @Test + fun `profiler sends another chunk on stop`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + fixture.executor.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + profiler.stopProfiler(ProfileLifecycle.MANUAL) + // We stop the profiler, which should send a chunk + fixture.executor.runAll() + verify(fixture.scopes).captureProfileChunk(any()) + } + + @Test + fun `close without terminating stops all profiles after chunk is finished`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + // We are scheduling the profiler to stop at the end of the chunk, so it should still be running + profiler.close(false) + assertTrue(profiler.isRunning) + // However, close() already resets the rootSpanCounter + assertEquals(0, profiler.rootSpanCounter) + + // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart + fixture.executor.runAll() + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler does not send chunks after close`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + // We close the profiler, which should prevent sending additional chunks + profiler.close(true) + + // The executor used to send the chunk doesn't do anything + fixture.executor.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + } + + @Test + fun `profiler stops when rate limited`() { + val profiler = fixture.getSut() + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + // If the SDK is rate limited, the profiler should stop + profiler.onRateLimitChanged(rateLimiter) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger) + .log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) + } + + @Test + fun `profiler does not start when rate limited`() { + val profiler = fixture.getSut() + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) + + // If the SDK is rate limited, 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("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) + closure.invoke() + } +} From 4a7403a937cc6f8861d21e872f0aa921e6b227c0 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 21:55:03 +0200 Subject: [PATCH 17/31] add service loader tests for profiler and profile converter --- .../api/sentry-async-profiler.api | 5 ++ ...AsyncProfilerProfileConverterProvider.java | 2 +- ...avaContinuousProfilingServiceLoaderTest.kt | 24 ++++++ .../profiling/ProfilingServiceLoaderTest.kt | 78 +++++++++++++++++++ ...y.profiling.JavaContinuousProfilerProvider | 1 + ...try.profiling.JavaProfileConverterProvider | 1 + 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt create mode 100644 sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt create mode 100644 sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider create mode 100644 sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index 74bc5a4654f..3e11247dd7e 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -30,6 +30,11 @@ public final class io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverte public fun getProfileConverter ()Lio/sentry/IProfileConverter; } +public final class io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider$AsyncProfilerProfileConverter : io/sentry/IProfileConverter { + public fun ()V + public fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; +} + public final class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments { public field alloc Z public field bci Z diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java index f3db84c55f1..bb78af134e4 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java @@ -21,7 +21,7 @@ public final class AsyncProfilerProfileConverterProvider implements JavaProfileC * Internal implementation of IProfileConverter that delegates to * JfrAsyncProfilerToSentryProfileConverter. */ - private static final class AsyncProfilerProfileConverter implements IProfileConverter { + public static final class AsyncProfilerProfileConverter implements IProfileConverter { @Override public @NotNull io.sentry.protocol.profiling.SentryProfile convertFromFile( diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt new file mode 100644 index 00000000000..5bac6d3ddbb --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt @@ -0,0 +1,24 @@ +package io.sentry.asyncprofiler + +import io.sentry.ILogger +import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler +import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider +import io.sentry.profiling.ProfilingServiceLoader +import kotlin.test.Test +import org.mockito.kotlin.mock + +class JavaContinuousProfilingServiceLoaderTest { + @Test + fun loadsAsyncProfilerProfileConverter() { + val service = ProfilingServiceLoader.loadProfileConverter() + assert(service is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter) + } + + @Test + fun loadsJavaAsyncProfiler() { + val logger = mock() + + val service = ProfilingServiceLoader.loadContinuousProfiler(logger, "", 10, null) + assert(service is JavaContinuousProfiler) + } +} diff --git a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt new file mode 100644 index 00000000000..a0963aec100 --- /dev/null +++ b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt @@ -0,0 +1,78 @@ +package io.sentry.profiling + +import io.sentry.IContinuousProfiler +import io.sentry.ILogger +import io.sentry.IProfileConverter +import io.sentry.ISentryExecutorService +import io.sentry.ProfileLifecycle +import io.sentry.TracesSampler +import io.sentry.protocol.SentryId +import io.sentry.protocol.profiling.SentryProfile +import java.nio.file.Path +import kotlin.test.Test +import org.mockito.kotlin.mock + +class ProfilingServiceLoaderTest { + @Test + fun loadsProfileConverterStub() { + val service = ProfilingServiceLoader.loadProfileConverter() + assert(service is ProfileConverterStub) + } + + @Test + fun loadsProfilerStub() { + val logger = mock() + + val service = ProfilingServiceLoader.loadContinuousProfiler(logger, "", 10, null) + assert(service is ContinuousProfilerStub) + } +} + +class JavaProfileConverterProviderStub : JavaProfileConverterProvider { + override fun getProfileConverter(): IProfileConverter? { + return ProfileConverterStub() + } +} + +class ProfileConverterStub() : IProfileConverter { + override fun convertFromFile(jfrFilePath: Path): SentryProfile { + TODO("Not yet implemented") + } +} + +class JavaProfilerProviderStub : JavaContinuousProfilerProvider { + override fun getContinuousProfiler( + logger: ILogger?, + profilingTracesDirPath: String?, + profilingTracesHz: Int, + executorService: ISentryExecutorService?, + ): IContinuousProfiler { + return ContinuousProfilerStub() + } +} + +class ContinuousProfilerStub() : IContinuousProfiler { + override fun isRunning(): Boolean { + TODO("Not yet implemented") + } + + override fun startProfiler(profileLifecycle: ProfileLifecycle, tracesSampler: TracesSampler) { + TODO("Not yet implemented") + } + + override fun stopProfiler(profileLifecycle: ProfileLifecycle) { + TODO("Not yet implemented") + } + + override fun close(isTerminating: Boolean) { + TODO("Not yet implemented") + } + + override fun reevaluateSampling() { + TODO("Not yet implemented") + } + + override fun getProfilerId(): SentryId { + TODO("Not yet implemented") + } +} diff --git a/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider b/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider new file mode 100644 index 00000000000..885cb45e41e --- /dev/null +++ b/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider @@ -0,0 +1 @@ +io.sentry.profiling.JavaProfilerProviderStub diff --git a/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider b/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider new file mode 100644 index 00000000000..9f4146aa9a0 --- /dev/null +++ b/sentry/src/test/resources/META-INF/services/io.sentry.profiling.JavaProfileConverterProvider @@ -0,0 +1 @@ +io.sentry.profiling.JavaProfileConverterProviderStub From 03a20dd44aec5fb6a41e5d47cad64d7adc1a35b9 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 15 Jul 2025 21:55:30 +0200 Subject: [PATCH 18/31] remove old jfr test files --- 197d8e97cb514418b15e5578026f39f2.jfr | Bin 84069 -> 0 bytes 249fcba726d5464b90d2dd4b2b24ad91.jfr | Bin 99364 -> 0 bytes 36354ee63d9240659b46ca78579a5c64.jfr | Bin 53647 -> 0 bytes bbc481b114554993b24a753fc6874fe6.jfr | Bin 88140 -> 0 bytes sentry/test88-20250408-152005.jfr | Bin 57839 -> 0 bytes sentry/test88-20250408-152039.jfr | Bin 56883 -> 0 bytes sentry/test88-20250408-152146.jfr | Bin 85992 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 197d8e97cb514418b15e5578026f39f2.jfr delete mode 100644 249fcba726d5464b90d2dd4b2b24ad91.jfr delete mode 100644 36354ee63d9240659b46ca78579a5c64.jfr delete mode 100644 bbc481b114554993b24a753fc6874fe6.jfr delete mode 100644 sentry/test88-20250408-152005.jfr delete mode 100644 sentry/test88-20250408-152039.jfr delete mode 100644 sentry/test88-20250408-152146.jfr diff --git a/197d8e97cb514418b15e5578026f39f2.jfr b/197d8e97cb514418b15e5578026f39f2.jfr deleted file mode 100644 index fd255d938c5e68ab4d070642fe762d83ec2625c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84069 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 diff --git a/249fcba726d5464b90d2dd4b2b24ad91.jfr b/249fcba726d5464b90d2dd4b2b24ad91.jfr deleted file mode 100644 index 9b54f947367c2f4c3311361478c1c8e08dcba81c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99364 zcmdqK34ByVwm;r=Z#wLYXfz!bqkzneX6dBUNpxTg0VK+%3F^$eH<{brx1mFlj@=21 z^WK}wzVG|q>>wZrs4RjFDElf30tyWx;KJgDAlvU-b?@!;O}aa21>gVu;gjlnt4>v& zI(6#QsZ*!wcIfnyPRHrwe`HWg`QeKUC8k|$##GCK7P=C=bHeGOSRH9(&ilT_cAEao zj6L*+tlB6^I^Fi&yOw;pqn++9{?*vb@lh_Pvy`t{B$f&`p;X}Y)**2&8!AoVqm}1qr<)aAziV#192L*rYdhWYYmSYtTb_ounzTFy(~vbTFjrlg=Fe9@ zFdaE#3T(X+OZ%0G?nKr+rF9Q5FinG_C=?%+N5copb9Hcm1}u_GG4ZwCj-rx6`Mowv zif7sC*hIJ0(dj+U&iQ zz1Kp269=?DhSJ5&kw_1A1H5&I*I}aliRPzg0UE_{<`JiAFYUi0LbUEU+D=F zaj7YMjpwrC*-KO(8WqK!A1QG;?T$jxr7(UcXOB)|Z?O>~c4aJ%j%3fEyNTE5U6a)F2x8&dreC3 zDDm-aL6u$*B{gkjE_pVo*-m{zH>B}(i{ujIrqxsPDX%3ivA4rn=I+|380A0QO?ky^ zz^aXx<7)`Y@NA&QtK+pkZKh1oZi1^9#W0Q6h&uEu9ff>t2GN~8$9B%?)G0>+$j{B`lHESP zeYdXJ9r;)Vy?CcRUg=A@HogwUH@jPRI+IXj&3sL~v1yZU*cHPDG^E3#pSx5nVw_GV zM~Jo>1-*;9LxAu#pMSY?JbMCpGx=JGSD>YkvwYhFvl12^^`FbFTb*(n`JF^ z^cG)&40hS5rT}Epfw;S0u{A*z0rBO@4LTG$dh{$+Vt@yS`H@asz*p{fI&t}5qKkyS zS(x=}bQb!?(}%i&#NulpS>|1u&YtSKR8%l3A($WVzx(;AksqCSe;gBwa$9o6Qc679 zk_?PWZA;X|n)0xtN`ew4s}J?mx0jh3#ER{jn^SCex{BoXq1a5GY8`=*wVdvkp~cXt zvNn>`=SE652ZHiO=xPmUZ);+$C1rdKHyuQ!ZoWpY@}NhzU^dogv!7Z$`^Y>Fb*ZD^ zvh@*Mq9QrmJgW;`uPN0CZLk65hrn3MLrf21JS1yPh*FWRL*dnzx=6@!^(ccP0KDR8 z5}n)xOnz({FHfrp$PR@<4>#YKLMe1~$9KRa5?>BhuK?1d{56ir_yFk`07~m-Ou&SB zeO4-)rDfq>;;`|JFb6QTF$eQ~DN`kR10{M6N3T?BuYx1Q#nIm9H|=7r%;7 zyVO~trqs!EmiQAGi#?a!Q4~sY%RpJV5))sS#>A^p+;iFL%$LGP(Ih^qhZXEw50iI5 z<7@S#Mf_QdcP_+lfwx-Y6G_QNdm*?@>jklVAb49;M7KFT~2%S&3QYOD?96QTS{q z9X*`rdnS}wj1zM$)C9VefQZL(D_f3$jRbe5tLsPXUcZ*oxCS$ctbSWHG(fVi|ero7z z(w|!T+VrQ6zApW#r>{?c8t5C+p9l1f=+A@thv-j?{$cv_h(1|9pz8Uvu z{9}Z@KTh9yuDSZ&g1)!po>1Rg>6&u`DdHMYX3foEFF$&S0xh^U?U1_Fn zDgj=kZ!f8DxiacctFU>ri0=9>(F{@KtfFB^f4Q9Wuh6msNR78wRb>B4Oa7bw?|MZ3 z2ltTNp8wPt2=@;f6bkfD$h`o* zsK7TMdoO)+wI2#;`9*3!6w_XE>Pytr*J!FsO?A`MQhk}4+MA{Z_94&r(f955x=hx8 z=?pygU;q6-|CR`a>rc3cWRwOF9q2xgG#-Qmo*zuOC)JJtr}F#|!ey%wLm9|0!ab|T z3?~$e5kx%dX0!2kO1(cvo^6Hw7uR&M2*#*uOcAJ2-L0KDaHoQP^zKl=M+ zl0R*-HVqg~A^NGnum;0$+W!iMz+yVl&j1#+7#1_Dg9XRW^7qtimSK+CQ`I5h+yDZ4 z7?I}%kbOQQ=>k957OG^ULj&k8BKpNZw>Cp}$?t&f(g1XqG34Ixw|=?W`V}l;r9a_K zmH-A`RiQbnD>UaVqF;mN$m3xx66t_gNB;db%NJrqaQr)jt1S_k0s^P+JN}frfhnN6#OeEUs>mO}=?qb9 z{u{^*;s$eJi9du>yL%|IvF_$E@0O^Oo}1<##&Lg_2{)XTHiA=89I0edh8j>A#py=_ z6`81GkW3k1Y&ax%ejLY@sTGcApc6QCv`plvc%H=RC;LmCQk_z(KbxTVshoZqikAn{ zbfx&;eNvwRvdTkmCPP71RDj+rPCwhpOupVS+4^;|zccu)`J2h=~0)6e(Uzo2^c zhonEpFZ9>3NT~z5SKt&MOc?_ji#dHbAVHiwu2!;I-(ZZU6!a#ipHmL7sxrW8PH*!U@K)vYHJm;l>2mk2Rnvja zI!^x&f5O`V3GZDsL#~$3)>9^%@>NT*qM&+Va;dTjPCdZ z`Ty*|nqUwXiv{dy+vIKIncBpf*wV%x87$T$S(DFRA)h9(M}@>`frZ+6J#0BnD@xQ< zX2-TUaK~=DQ0Nxt1ZL@3US5z?N5$0nfq8rJb#0=htcSc0hrNEfIbNZ^>h%&`#Uf0U zbjuDlUWUO6Z7C1;f<3B(&`V_IYIiowq1IkqoN{>+`h(|-6rSe?kg{TI-98=PPH>^O z;!{kCro?3R?FCksYSvW}pN@Y8J0G0|SG+MZKE?R7G5zV(wD@lA^5U@%m-b9N^;>wl zZ*f_I(P%QJCt_1C(U|r>#-x|pcWR%N+de<*rOswkex5nmWQ-SFMP}0?ihZ$)k)Flz zb*Xh%jPGosmR3@~{?Ee!R*`0;(Zg3|V-ATRrWg0D3i!HUE z+g8vctzXLPy^<}jnOw>E*P}QoF9|znLOu{;k_$~^hSGdnKSbqY*QO+2E-a}cNFoDi ztw^Y+FrJ@c@}zYiaOhJi>$f&07rCF3rBdr+=To%w)=6~3$DLGI)=O|(3bAYMN`mcI z$SoF1;15#jPRa$1T>aX?ZWmxu&(AmI=hGg|Pd4SJq~sfuQ!`VOsW^SA(AlH)uO>i* zl_jFG#L~W{v{Gh9Zr!eZ+wRZBC%pRV(|Pa#5pB8DMqVnX8RHY$xzRQ7VZkO`Nm*$k zI^2<{h>g}lVIQF%giAZfp+t9CaU!-Pz#(*-BC(*TuL~3?70Zi2ls_vSisdPE^tXlo zjr(PX*e4MV3PN!^XOVybFD z7M9t>)_;mmpi+|Y^3y?jvy;4|kd%#WJJFr=yja@S1zg-oIgp5jg^uTGccUv!iK$6e zp@honEIKZdn6&93x@5*qED&6+DaZVJ?p8`SCc;QwLJ8f=iP2ZwHxWBh=n(A0yAxTZ z3NaF`aM7XL;)!emOJvf&HIr$`tfH65TJF!{FAC$CbxKv0z$dDYTT??dGZvMV()osF zEfo_(7DH(U?BI|B#%tX@uR}rxuj8L_I1}A$WF;!b|3rl~;k*TY6MA8%Qb-Is9xmLP z2|eY)cz!>XjU}qsi(@+jy}`RBq80mAvTHyhmEip2&(k@#g5N`YZXn#YusUhrn<_8BK7*!^!0+?U$4awJIgak_w+6(UxvXv!sdXM!VT) zv7{N(O;)pDOi4*kvnLzVOg4)pY*`7cEGn#n(ePfkK(NB|4L&?d3(V$p!6=wgP02QM znkc5*ZKBbPcECr~WJwiM(`-VfJ3V2W^7Cu;0B63iejJ(V8LJ#q@NW)oM>kF$z{Y_{J(` zTCFK2Yq~MRnrszJrcl`bzIbCemGpeAP{mzL2CoZfLAu3ghMH^=%_$aRI{YhAGSkw8 z46`-GYBmN%T`B4JMgO%j4CB`o?Coimbio82I0c?psV1{vGg)jIMsr4Lx>Yb5?WPp7 z#cr{t2Sr~l>G#DRylO8j>#LxjDx@cy%+~ajOk;XRrrn;JYEMrw*=%VUHltNA3n{5- zDVd@zDE3N8zc2cNvqYtQW~yj4*&t)g<_sISKHZd_Zc0hF+EUU5dupaJEk#JSW(q-Z zM=|#VZ-MGur3FdKZy!;B-jrla%uGz7(}Kk+q?l4|$$~xEW-+CwW!No(XiBrD*-e?5 zX;upaqs3x2rfBmgsQG^{ab94vSQV-YspiZKt6)h^wLlIE_?MbtHJWX9qb1dB%`m5k znPRGF3O*Upa_$LPEa)0Sy8VSR2gWkPCaW(wwXTdGa8o0Buszz=2X{?#0AeAeKT|#o{VaUUekNKM5|;UI_k>8N!s5Zcevnh^e;Jj1(a=Lrk%y2_|zI z+)F`BxENLlL z(PB(Vwx$Wj40}epDLKVTImD`>8BjtMs79#EVRdhTk?KBbi|PcD0yXTusiew;asMh{ z%`T>-8jUu4vdL~u zNwX$ftPle>hylL>N#{?N^BxnEE)rGj*n-i&IuOcAoiZ=LP^HLyTMDEnP|PrylkFz6 zF(WhGXoAjb%n*%MOtP6KOJ*9@AJr)FH&0uz>l8VxF09b8#7ioFRF}M&5UxgQYZKDV z@Mq1)uw|xXVy$6Hw_4M&R|4UhV$2lnCSyvP%_ycO2P*^Om6q`vh#nG6r-fRynb}Rn z)M_(>rdWmAMNl}+W=&5`v1O)VNT+dC(Cd>|ITOg(Z#r$?;gRH96uEfYt)^9Qc zr=}Rw%*kmP8A6(9$KuVFEl5IxtMY7qBp=QGhG_6wp`@zO)C9-QX%zw%#OSPn! zF^HhAreK+GPtL%|GMiGZz||&*8A2))#f&s-W~Kc11If_#i2}E?SQ%lW$(U+O!%8f} zW)tjYA=3;gk(!#Bk!}ZZ1<@=*B@j))mp8PW-#o6u^FUE=t3umMi&4O=ke+D)lICdiL9}KhXQV>`Pd3^7n1X?j`F;>q?LArkQ|SuXDcYff(m%6kwS$;ga3z}s zF(uuSo@Rt@YO<#Im$6T1uT5_^I z#RBOB@g%_9k&*^-`a zu_c?6EvY7}CDWcFSiz~7l*DvvhM?qvRTjDKPtRg}G82kC^I$Rr+ohROGBL32R=X85 zC4?o^JX0FhQC6!d)0%4YFPN3g_k>~_60k3bYSnok=-pREzy{m zoRneBG-KgzHJTu1>{b)j@RsBhj9)07X3?GsT_5^>hSi@hf#nOuk`XSAm7&Kv<#*Ur zVY}zSws7v-g0K=V2U`F@GNoamVi7QjLXf80?5Qcqu>V=j8CX(@7T5&S%xNYq4{Hj# zAG}*>qE%*AOA2&nT#NzBnk6GO!$ReTfaR+ZI(1rFveB9b8HS~R%KNNy?}d4)+HwL0 z7dV&oQ1KPhF`NWThAq{K8OtoBS~D{*5)7^|xtcO8nHDO3Ey-}Y!OWjwF{Z-)nu;ZW*=9+#P@Pr4nlN}N zbT<$G@9938+Mo0Qt~$kHUm#P2;IdgW(=)|%to{Te<`LmeH*D zWXk9#6cr}9i{S>wo-j)$WkPbLnz4TYZ4|@Do{2N|so1`Qf!b~q>`1Ue>Cb>lqGn^0 z>u)1bcx$9;h|J7Pq2?aD5j#LoJF%}Km{Za$n7T4EO!kaaGfdTHTZ*NEC6d|C@4vk? zT}9JmhFXua+?misu<*co8GAq0;j}$Kocg{bS#pwR}d1{$YzD# zV#6Gvag&Inej0QJVS8X#!cK3)*Va*1^p*xZPL|EV9Mkw{*)wgW zCPR~OUTVBTn(XhXMIv4S3+ced>$JWMJ>c$)$X(DdjI!l~RRcnOx=qO_}vHUS_Y0cl7*su)v(bI=90-{gb#rl#x%{ACvkxB;psF7c?F+r3MEZeCWGc3CgA)6(AW zs0KuEON$#f2y}%*Ag<}cS?<4yE@wQw^T75gt>QJ+yDgYsQZ?M2k}Qs5=1C<<=La^F zqzeNZ`ZVP}7sTJGI^|LiE&LS}I%MZtNxDYMzd5iW>uo>Y&m>jw_hQ#tQO6*6QZ_8v zLZW|rGwEgdgn>ILOHPt}@2!oycoD%-4bH8@l$ljh@`9r@SfSZTa+>5jbS2g|@W&gm zCp4vIg(($cQKaYjgOy8>l~X0(h~Wll?YxnOQ<{?7hbg%OE(?Iu9`*7JLWu07@^s02 z`9!RD;pH>2Yc-|EC8-{B=nt9LiEY(RIqx+r+<@RKO_pvI5$Yy8YzOpB_nh*YgJG7H z6bK=CKu*tI-!HL0X)5}Usw>KN7Sg?85P-T1Q&I-6t}>h?E!fuF_sjBa%_FY#CFaab zE2Pfx4tTuxUfu6%2%Zlq`A^5koGP+`NVgc zhP+y_A(cnFe3eY+a)+xT>k}m5LPxQjT38gnL)8o2}et z7Mwd|Nu~jmyhD%24ts0G@o*B*jmu_iWkn^51oWIbV301IK47?}LGeGspxC|GLGPxb zcfyok0~CV(!C@sIB%w6p{4cRgiS|#1j|7Tpi@OLXYS_sYN%~@=;oF55HX4}H?}z_G zAUu@Jd|AE7FM`83wtv~t47mW$m6?Xs{jbh6e4}aROBI`0x$7wpXB7*D{Yo9y5RKG; zNZ#X<4Ib~8lMREmSv>Cn|T3{3m?6FWAL*z~4XhfV1n?`LZahb|&Gt!Z(G>a{rc<>!K9Lz{|3$qTtI zjrsgd!x>HS9jh)rw^zST^zOLe<*TWZ_p7_H(&;q=3}-bJs!V1n!Wrb~-^Ecqs{Yb;IJx32MeO%;y@ z(z>(Lf+HZ$(M!_`OI7xJ?{2K;ixYQa2U3fgpIHkkq82E01eYcFIw&hC0IXw9*go+m zOVW^)2H$D$O7)jxFEXe0&|OfTE_s(sGx$E5HN#N-Wn`{$;w3oE)l^Bk_*JZTAoz8n zhIFt0cGBU*n`rGxFZ=94Z*vSq>`Gvg=d+uJi+4W1Y4}{j1??)LRGGIaExYFgZ?T*v zdET0VMaIyX5r|!ylMTMm1+!$yGxL0`@6v^@VlQeC?^+S@0RC#{aQDh`SsYlQg(gu2 zAjvatis7?03#LRq_&b*smf|AdGHcJUYnMu3lJDn32G8K(hYjy&n$|RMWQw@_w?{vA za#R_Y62|344}yqA#!E02i>|hzWT8JeEUx-WTTa&yR-3FOmL&OZU5@oE ze)~!U;+yhF_-QNL7)3-wm=Qa3u4ue1CqgPwlvDl#J)$PGn8?62SD9L;J zWHagN)sxMt&qd|xS;&c110s37ruZ#~uQM@>)vJxpub$w!ez7qbb4oiKueN*+}A z;hQ^8HLJM25ZX-7`aGv=aKb5xk_YQo>B@Vn4b?AKxuzv}xk{qsh26~edH)fS8_zo6 z@&p)3*ah0b>j;F9d{Ztp{p>n|3!2X8Qn52Ci~n4wy%aZh_kyJ^cs&6ClIMcgaBdv} zucn$-{z=UQS z43jfJrYL5nT~ixN(w?b}+1|2${5=t-3|V3*lgz29jSu~R;5`ir%|m3Duia4_ezb}qTQt?T3{}0ujY}7-J=NS(N5#5`{N;U z%NmW`Zt3*Y#_J{{s9sjtf~Amb*6qd{cFwt7&8%H>JK;(grL&Iy*ksg61YS*ZL$R8C zb2#p9$yW0FR?mw&b?mKqaiNToT8eiqKRKZk;N3K})wJ7Nrnagc0_AHfHUHhkO%~rm zuus!~)DRR8Zi`jR8oH;+lyMk58#HCbhbZgSZhk84o7#B6Zt!gNhE0B3oS%cyJ|BDG zg1Q!#ewy0q@<9aKG?o7`MCJLgdgE#Y7xpvyseOUWx4xP@t<_FV35lUfkiSlYE0pL} z{!}o@EaBZht<}=^zMIyndIK>gU~ zcjE3IdQX+)!+?hxCZ{2rm8&^*}x%;{E1Mu}wRNq8UJu;OQ zJ3uIv#K<=82!b#xN!3|_>NavqJ>PCM99r$!_UnvKn3_ZB6jaq?$&z;#wjbwV4>F9! zlUk5vgruqa4Bnxe_8H#QuvxQmHe;JlbaG{Pn5eBk=QXT5^QG4ilJ4c%;5yB&5-E9cS)H|)U+C;V z82i<>0|%?oqe^zG%KYmN#a_Mm@uAp|(_6)gt$v3MwRY*W*D!06&ud`rVt&miA;`E> zYuD`m${g)Gw#MN5=F>HX-I~#x9JZ8F+%_QMJ3{oPIzQXIzsxXhD7ITZ)D-koxPst@ zr8IwXc%^3?xomW8XS^B;~;0ZIWShS3*evkw@z>M z?a6J^L#wP6yT?}ScB=!=gRygHBg9*^J6FWFs8lwg)x2iTZ;kr-7x1D~u zNFj$0B4gG@uv*nZQsEHl=Bi|tJcna1URr**`pjLV_5yal)_=IyTm4QfVpp1~*&!lV z-76Z4mRQJPxN+jnEf-QEBLpD}>ILTMJs@2~;6gz6in8UFlmm(FbFM&%mHOi_am~%Mx(6zaTt4~7Q z7EHg8sv>~8cl|g%0tDRsil8JEsRZ=AHQKOr$=cD?P%@Me5NNdbo;esRt;V!n^$aRs zEUcmpz2wu$$GryTf$EpcO@fG_z9s6E;KMv3twD`pY{xLT3KrG%(*t9x=6u7@_lwWcHHJ~FUN*a zD4l}3Zt;L}BL+az3gdBU#~Bjb;)&}VnfmXAv!z#pedkLJEjm?gMxr}P%vcg)^PoIk z^8Gk7PMWxIRs7u=?*CQH6; zw_`oCAq0nNHZ+SX5+%SSAJ}`r(GCFakSky290E{Dl{{x=8s0l}Zl+LCCPIs)_WYxc%LXjO-Qzo4$Eq)5>n+ zmS@rOEr#Km&MCQ9os%mHE^E(_yNv#1$+uyvLE3v{YgL}80sI#cvu3pqPK&Jek~D6# zVc3-wqYV*k{0GRx;N=G3*@>M_2;U`I7`8>^o4x*%*jbux89?q;zqE5O=Wkqb8y>I9 zaFS=mUW0UI>)!iCu3Vg>5^Wu@WbXuHNpR|FVI&`TW(sw`hV4l)Rhei!OGg?GZJ0dDa7ZJs)5G-3bGadA03=AB<7W)h zKE8A&au;)as^rZy^y$`l%|jU{!7Eg01bw$(pFIVjR%9~Y+W1;G66r8Cm07>5Oueb1CA9|xj#4f+EK-2rG z3fq?9T)HcGZCV(~i}N$m0C0E22P_zQJLG07$Erf^!d8aE?%x)}x7(&~2{-<%=!;S~ z;wy`)&o>&zT|67wQbetca*lkfy{A}M60n-uz1MJY+@8ILFz(?cLQ%PM#@CPzb75`` z1K(G0`CTA<=l2>$t^9g#IR8jG8)CDkypEe*!^hh(_kF5islQZYsetJOgvRl*kUQ!u zQIe)#GF-oS>QdxxHRUIM+*N7%>JcJ8Ap9G)HJ|q7tzUIEh+S3*?g?M(OY)_b0dSIc z*ZVDReZJ@YmJtt2<#WeX)F^)wSp|}$?>}t0diKvBwhU=-)a)havNf_geH@?K>iVVA zQzMrkZ1l)w(NACpAJY|pB+tn;hFQzuLLadN!G0#aiy^G5gBC{e48$(=ibYc!pV6># zH5}@@Is1q%x`P^2e^*l_&#u97(sxG&N3d4PrCmWcrGppkPnM()2R^#Miz7o3Q;MFa zs_F=j7Dn=XIm;k@b7fXIL1A&y!{lzisoFc{cC2p;R&=4P@(_G5LgoV-`DC3UFISE> zcn84>!p@2NO%V?V8VO!2bErlL>HD#brIRbiH4bBhz`4g#t9sDKJLsh0)(-eM+|+b> zH4c!eLYJY4Q*qsFpzP^*ySvCye>Pa$^`Zuo!Uy8h+!Q@g)COq>x!)cop@*`f{0ySy5?4K zWGDbY@=TiD#ItYt>?RSLL}V! znB=)NCT`s3_s2$l;6Zk73R|591Sef5H1$nAJE3XBCt%tId^0OJPX|Iso*%}<4H%7v zg&Ew8>RF2Vpm5TFF|C$TeNS=jj4Zg1zt&Q9%q%AJJU|kTrll{q(_dOU4K7rLB z;w5@DdRPO3ezsu~ouiz|Ui9=OK)&Mw{<`9#(hl177m zC_+{Mle`0G#`#vvm>C!G1l<9{CP)LJ0n(Iif(t}Yb)3V?4Zb^rR~n9K#z~us2TcuSBVNoVfH{P-J46%--&0xur2WMk^ zn?9LkxUT7f{EGa`v<&%v8|KFwaOMZLV)`k};LIEXSn|O3^ zNaFh1hjMf+QV0ZIIr& zeA>XiyzI9M^}EM7bj%5&r373yQJyY&aq>>uIdrIDoQ5iW?lDFXF~y~Hb_ve2{$$B_ z=ZL|(9DdLPH265~F+MN03y#Q(?IhpcGlr|b>_20et-)YmxGi96BT(!c;VB=$DE=dm zogmDHPDj2NqMXsb z$XVbBMFk~U@{QhL@GOUsB4W!>2;DtkR0KjuzDYkeKX(biNzL5Zwjv)^o;$ndg}efX zB}v}B2MnHrCl46rX&7wiy=JhGlISJ>w@;Oo%S&PpYNS;iFnze?!8aMV6ol~_>)G3` z%GI~Vx0*J5{rJeYR_KBW%jxiQdj%lL`@^)Bt5x zT~|M>_VFZEgl2K)TZnTui*UFo;!6dXG4c6s?L*v=IFEykM`*wdIJR`Zr8eznZ+94jq(U zlHNaJ7_{}+k^9n|=W^6)yf=;*7GC+~{#5gPH^0NnrbC8vgR!E$A5>%fBwMCEUmP(E zyM5`1;aiQ^%B?82eiakEBw+oETOZUOI&;LZ?;`|NTzOMjL_XK8JM~(ihl7Ig9?Z6kBA*Bindlx0Z#lBQFXsccco?SCqcs!rZXi){vS4M#mpql`v?GU@40RXt` z>Br{2_xJqRy!uAZaFx$Uv2 zS5JQ0gWY$a0g=1|-iY&_o$^N9V9f+vSaFb4=9+Fj;opN#p}TQhfZp_3RlT9%CEu_$ zExr53t%=-uf?Y%Rvcp{_6w)&b0aOl9W-0)|R|6MAZh_`e{cAR=&BD)h>aMV^XW`*y ztIw}F+^qWIr~0*Ycvbd3RO_02xS4n5)WglHFyjV@y6V@}Nv&k)N6ii$8~#zV>fgW- zK_#*oLD^G8OU_LP{i=UEMzw3oez|jQx56?wmZ*DVN}aedXZ2Xnu?h#CUqeD>O6;t? z&<`?g*aTeFQ~kAO2s2@Do)18?b2s?Kg<5SYiINX@wn!&&4zK#F@!xs|`fG*FUv@Fs(2!L;`blQby)tD^7>>|?5f%&@Sb{60}65=*Ope83vo{5_c(g$-l8y0DB8hM|{_L9(#ty$dc>|$_U1aew1 z$qSlE2Uosln4&@QzwZy>a1p=D6>2YpcB-5%`9L-Ada&J04Oac{6X`nOdjWKtFx0L< zw_HDje0iaDsVCc#E6 zyVWQJ|IjhR(j^a$BY2jaRVkI!i_wnY&p25d9H;&l)(F=aR zA-u_05^lCz+T`MdJ4>5Htmi>jeM!KT`EUyjsr3YcNYeXojJ$y$0?YHSFyz6`66(fB zmLy4+v07ec8Vm1UHJkAi6C0mPUwirKU>KCua=axj6f< zRmNr1U}koTw4;Vco<7pAu#ed#B!;oC2+c_VkL=_ulUkWklI zD~XciJ7Dlm8+Xv~ou&#;&`Q4caW-bku!7B*pp6m|QfUf|Ai`i%5nsl6T409{86-SzI5Vzb$ zhQsl*ZXh_J>5{B4b8yzn9m3qas-{Yw%SW1dw{7~kDpk7jOpx^|)l})~8;iG1!pCeB>8a1z3(L4FRG9Wm1)9IRkrg( zFB>XC!Ef;ke4z$+G*+LRXz87E+J(6wn5M$xeodU`muqX{BHreZsZMXf3gw*`2q8&} zv1KzEed5z}WH?WooUUyP%fwQA)Fp)E2mp|zAHFeo$ItuLP{j(fa-T47n)ZF6PT(tV zp**510O8vsuyU;2d?4~2s66^%0gH%JNI|8cKUwl_Sonlz>yCv_+}2RcR`tm9kGQUI zCdTFA;qF@w4vTXoJDW?FuI_9eu}5iEhZpE!h~TYL5+!Nl{wALB@9l3A^4bcGW4<3r zMN^QxxHfJ8?o5n)0G5={ebXv@c(FU8fs%aKMDt9>Cr{33`l1>qU#ONE`oOrFDoF>< z7(B}kpNV|CQ9d!$&53Q{5L>%}5Rzy0xW>}14dWVz*>cP6oUK}=uK5h()DWf@ZI?MOyo|rOx#p)d_oH&`DQF_E-hcY zwE4%HZVmTsHTfdQphK4>NuJ$nntB%QU(+-Ufs3%wtr(-%Ol|DJRgR7CmHb7A z+ZWztrwra%_>w}ztD)@9*+L1uc!-X|;KN!2f{)c5F?deEy`uUb$Y`%#>{FHNb`U_x zJLz;Y@2wrDn?*c0X^Zmc)r3L&Q%#k;IEx|;+%`5Y;$2v33(YAGc{zeVS@KT6p6XZg zBT?ZgdR0u28W4O%52x$qeh|s&IyPQ#|7VCp`u=3ehegZ8uMtE{B8sEDBNVa|ShD2X zvccf{V&=xkXLx@V;2xaL2;JMPA{j{XzH_>{_saXHBiM>#N&@w5gHjak&hyG}IL&+4 z;F~t*?ynGgrOKfQas`!{u2X*1D?~3n>wi- zUT=_2z`uumli2SY_gY28DxmOt=djo*t6Ns~3koK_dD{%*hU2!4@fw?G-`@ul95EMM zS%p1BY!a)a!D$xXIvC!^Xb>v4w0N!I;uP@k{|5WYeWX$ZC(ec~z_$YKZk=|#4B6%n}Olof-6Ev-kGE0JlE!o zj*GZLs#U@5q*&}yR)igaFltpG6pnP9HGH^x-`U8Q(oAg%sY|P=l6TOMIN!N(L*lA$ z4F|u&Q^@Lc*+P10X+R{;vB7bk#i%G^F2EjmXj8b7D0#3WGi@C_u*Yhq;ocRe;mZ79 z=!EY@a25+tXBd<_+((N8AtWzOXGyb<&Wc<)rC))pYDuSsfvIJ`LHg?Q{%{*dIbG$W z*s9iotI5AzizC)m9D%7!E;j6xQ|xdOq?_$M@NM7_hLljyl4tJX=AQM77B>&G=FjWY zUPbrt*jAohN5;1LSz~pm#)Ba(9E|FL72;qibShm zQD=92Hutj?IO7~<2QT}j4!LExJWb7lPYAC+w&8f>=g_Gz>C&lNsMfGVN%|aaG;?qe zI?Tc&uczS1bM_K(i3g03^1{Q1O9!u?#zhw4T0(759pC|?tZ`0kG^{@V*{?V)O!wQV z!A~~7gxqJT1SWa#0aDM~xH$5zhApbI(^5=75LrmKTEM`?_-GDw`wYRvIl+P95%hiw z@278gTSTm*(7sb=oiLZQRRY72pv$qo-S_}q#Al4&7QH)g4wZ&J;+=9V8fzAF&$Wj}Z-EM57_X+!nShfwxcwJ()?`0CQ9LlAg1@M_m$Xl$w;EEnV5@lLag%MQNNEMglpZ4*4>uyYp5 zl_C&A@{FC*6y9l5nnr9S%z6qA%Hj2R)6j$1&Ry?9VVpGKfOm-L%D?sbR1-@c~h zUmetNgQooQk9|3e8O4+a%uOpl;6q5KMzkOD(XB4dL9j_v zS^0P2)I3E(33d_lsbEvc?L$qwzD7{-48m`sgRL~*jc>!i%SWO1DB7u`2j5$27yCc5 zCHZLiU)<~R%kiHu66H@Bd2g8#at+^%31rGoX)}%wZdh^W@i%owc{TYWK5we@$VFP! zSA*}ZY()9RsK$IesxE?XTGjK zkCpmEpYLM|z8Jf1FCzb(xAR{ta^JrFF$Fg-F1w9L8bSZHW{_vDytV4PfXL5#?2ai| zGw-HPiKOMWPH0pkrr^ubW5$0U068x6t(bx%Q*UomBA@wq=f7IddTnP+!P?o^e+-2D z@V&2M3g*sRx%da*2`E~78{^I}{IU$V&sS?F)!({n1B9VhXO0 zx_T!t^31KihT7a#T2Y(&8RudOJd1ult3)z9-}!rDOu@MMyC(h|P!YfMnV5n* z`*wT~7#Y{ORZPK>T~mGvh-}@7cE@n&KKx*35yl%*mrq55Exr z$@*ybzzZ7#BHOUq$8Na&O+aMpUv>;bcg!3+=a+!&JDa#-3cgvk=718(D(We&h$%Sr z&HHx|Nh4S@S?#xfzOedcdGyBxA*~>Hm%?xNvIdH-V6hLyqlOHtTMA_BKr@(@vAt9tezVI{bJ{ z!Drh?z?90d#%; z%|~Jij=g_w`k?Y^+B`r9;Ty-d?m?t~U^8Ly(a{fY1wt|rFl*MK6@$yOx4uYuZt~&d zrx2-TZ@s;)F{a>ytGmYxks-?)Ippony-Fmb`-p!+{9U&7CFf2flGe=dOrQODOu^gh=ZzZ~P*I)h^BanTR*V@5A`d*l&z?r189N(<6)#`YNEKE5Tc`zWNC97gX~+-B zB^V#FZ~1KH4a_|Q*Ymdzb1(k$?%hWpw68imaWEzn#9b;>W(5v*oB|f7Cr> zpR{Y?a{2Y`&v=o7JY-*eb+Hn6a=#k)&Cz$1xUEb0L>aFCmNQD+^|yEjjcX{LJg{hm zT;9ebobEyU0^e5UeZW)o%zRS@*>`~ao5nqCpK|=`RdU>%v)or?SUfB2=zwV_ zF%$+KAWPribPdHJn}_^B-`De>%^^U3^(PCI_X!tBu3Y-H)6=F=lCS5t=BIc#R?grl z#}9ymQO?_IX*s74+&Tl6#QU2+(D(OOuD%Aw!~5dFJl^-rJ;1u@09kQ?rXQSeaz8p8 z>Ar~s?{|+4eFuGw_v4>(=+=2Zc#Z+DxpVZx4Z0Zn{PSNbuiM`xUy%%k!-mhM)3yNS z)+r7^+fQsz-ruDyrVW1B{{7^|r(~c-(~xs@42wOm?;9m{(=>pNlN+`E?r6qahxoZ) zQ(_$6F>f7^LIoQx@~M;<4aAQ=7)}vI+>m_WJ>b^D4V3*R@JDvhEaOKn`V`eI@RAKD zxEPwHp?G}2?fbp^U*lyQ7jfl^L4U~ zes}|~ zqtqy*ecOe7`7c3kZ*^e$Krrx+$VAJc%YInbi*XPW{_Y9M;nF&SU4x;V8Y=$jfro-- zFoufHz_bvA_~{{{O#{yb15XJNorK}YKg61xP$KU&x#Fptq$^~?Ymgg!(_C0rihIyq zGWCiog=;%HoxQ4tcrY#jLa`;;6RnHpI@TwA3`e4tL~%s_c~mZm9zmi=4IjA@MQTnb zuaa8-jv}?+)$2)}?k!2(Kk7-n?haBvHJ&sW$JHeb2Xb}D18?w+NuvRLCV6mIR2}lr z?kF>fnTKK?UJ+e~JkpntSczXjh+$0BR6^o(QKaz;%}A3oTncG=n-J1$Qmwy|N8cn3 zNc{PlkCDeFk~-vZT}=knyz~XqVi$_R+}Nt+2VCRmC;l2oT5ZrbB7e9Fod39m+sKhW zeNG-EPfq5>apb98WB?%vlM#`4O#cW;>hlmu-W3&1jKiYpk(3pDG)dhDEKR}=LeiFV znZ!JpTTV#&F(hXU=f)F~c`IsL4f6E(s3h`CNiFi{q1@MmwD$6aq|F;VNB*)uYC1=r z-9%DJ))=(1?Kt8gBzrPi(r!7|kF@_0E$J{bDvCTejB7hvV%YR&&3&)Gi6P6hhP&bskJe@srWSxgqKq zQt}@6EP3q)d6y%uH}xgNJy73*lzv*9BV{L|ULd^>*CwRT_F7S-*{oV6RdT@Ad|i{i zsJ0(aeI0fEt0hnV>mXN?{P!Btj{MIkz8?9vkIx|z*~7n2NPlvG^yk0~D+s!4AUR*B zJ{d&D*QrZd@8n;WF)!E z*CwOLr{JH_dXlQCo~mqx~tpY(BL99gD!lJVqj6i+6Q_ecvek&MRkByyERlgZFK znvp4F5jbip`2_7D)5uCbicCknkCPeX0LH;gvNI}{%p$9zx{=xBTmA_$hun&KoXjQP zMb!m=3LKe7%9<+Bv&hP*j$}S*PRIiCGm2VBRs!usL>EmKlc`)ivV=Tehb$${qGXPR za$IK_COG~L1X0Uru!6p>L_ppoQ=(@QvWk2hHH9au$(J>rs+j*$fv(|OXqk?zAsE?= zY1fk7sB<0pwQNf@zxr=eRNkS%yEIr&gAFv;NP|r@K&`r{%{16TgZF9RrNLGN{5Bdu zx6`%WP7XIFAZg!!kPpe-XhL?7lhJwTqU-vWA-iZN7_1e6qNPV`e9cS99`YqgCws|o zh|GOtW+N-vk6zO&F>?F?GL~;Z4w4&DO_lHv$%-L|(TjgnB0eG_#Mlwi=_&FtN&8cf z!UGGDp(`UGSN91qH`LS?yzrys`+AMZG4ci%f3Ji#U1Qb%lv4OO0&;@9ZY3u%GXF$A z15tSLImxI)PLZ#pqsVEJ6GeQanI3#}hp!);jp!ny?io@`7k!regOI;}MLI?#>7f1V zntege*RDyCqI%65H3HF6SbQB9}`Siu<9PZMrPF1k)JW%>XPe_FdVr- zo{uKK5OXZa8^oQAzNgbAR<^qMn`CCxDnf3N361NK+vF%;gWMq#Ym6l1E?HQ|NF=U* z!~ewm^E0MMGJrb*jvmN;!p-5xAZ}Ley@U+rUVMlQ;l@A&4&|2cb;&U966Yerx#OIP zjNp#x>yeS%S$$11io2m798E@Z8|(QvGKM=|w+%XQd{jd$JJ)ehWIVSTWn?cPMb(|} z>6)+B1g>K(-9&CY2Qf2=dxV1&pAJ%*BR>WyKi2fFIKmvVwGlxxi4_HYK>Ty7N5p)#dTaG63^ z#I-!MQS%T)&8NWv8ibrJu;$mbS;!6I4P+5_jyyyba|5GAvV=PsRYaC@o1-2g%eXgc z#z2yOz$cRB+?U)Sj;!D|p?_B*10ipM6|%`HPS=>M=5$TSTig)xK1bF7q$XL*O$RO4 zabvke@-`PAMc&~q)Ye0Z`hnCU>$y1)UK_YS|5=$54r9_!{r}aZK}`tvO!`qXRg;ah zJ2%nbJsNCgy-S1lY2c*+%ow_;Z8X?UgAZu%Aq{rWU?&ZB(O@?X_R!#0sv6YZRd+8f zpuFCF^mRWCs-dee3(Nrk^9N~ghz5sg-j8Si6Q7R%mgU|EScH8@{`UXzH5fI9K5~k4 z>!VL|fBh%%ahpj`a)z6Rd6Asut`iISg6s7-$(v68Yc%+o2G?nDg9g9Q;3f@j(cm@$ z{tgZ9A|Mifqef%WpPzzBc?UTaT~>wiG=MK0OUOVT2z*QuqW+I+yfBvQ4Dw215N|V( z!TbPN0|s(?xerLlsec4_Qvc+Ao9iGh2>Apn?f>IFNH-X2v$gIx zv@>cv4JOba{1TEH${(Ic;U>{wG7Y96;HM%W)A(;{=*V<_JGD&A;P*mY&g2)u9yE)l z&gM6g`eY8j28th<%a7z1a>T>$fF3)K$Ld&XOPbHG=9*NrDAApOdp2j%Etm%u&;~4| z!95gKi?Ny0WDzZNF%6c`U?~lj(cldllux?L>FWv_+`rivTW7lXmHfn-kCQiHqHIo9 z@hfT)vYLO6BX9Ampx&3@{^E6I&~XIs$rpcJ^ zhyMwK?p=N-%uDO}bKI>c(seS~PJ-A9?=>dae$+j>f&Z^4@)3rn|2E6_nTWFI_{B6=`bH`IJ z)Yjc0jqY#*I&!+Zq|uMuyW{~~fu+SF7)fedZjrXNbowtzy+>eW`;xTjQ(G5P^U+0w z=;o95BckgJi`RKb`@7si#LOn`=fDEU>E@F5XJfD6VHRocVKFmE`$e3@)z-}VD9oRIZbh8onJbjpjhxz0U>qEMkczTok_|Ljnj!oj^ z45!FeEPkM#>sH}uFDi=CUB=V&WQs(MCvTCiL^qmm(K7u{AIF@e2RHcH~TtTxxu zohQ$Zs5J_e-z3j2X@3QElV{@oMRebB=NtV(ZX(Mn{vvF&Uli8Tt;Ev}{=0oJ0PDsR z*T>}XDBah@b>`2v(ZY+wwT8^kq3i79H$#gTz6q1;B@1N>qiRv6>+VDNrBV- z00Zzga+@Q%AKBA7Jgwu7lHr+y9;>Nai>Dj^y5Ov-8^Zr#8#Fte&Y4!<+@I)X@WYUA zr1#GwVHtMVCHS|!fv!; zDt8__^(s{J@L^)71r8y(nQ!179s(n3eTau!BzH;s^LW@!wt$FlJXTBh1$mpyNS=%Q zQ@FQT{{Ecq93r=Kb5X=d=q@P(@h}PxX(y3&1RmNy!|8GsabH3SZm5etz+n`?-gki8 z%~9<89N=cNX1HgP1zby_bIk-TUW(F{%_LD2vh=hZ-}|)u-fKHyCA^wF9{SsecwP7G z^sDrDEy@0*F^hSNWd8u04C1oRl3!2_(T(HIk^`^M_Ko89HW-LFPB$7)tN*+hPh;?O zjav-35!^E9g)X#(J*>!s3A*EWG8jN@@JovtL^_^>5rNYUBpti`k<&f#s;Og??sM+d zWgS~>jn{ofA7-PX2iKBlw5P#Z;>^a>y_TFM4s6S>C4cBdbdA;$Bi=5-@St?)MQ#?i zf}4j5F5pR;K|XnAF3>4Q5=O?nV4AKrvnM`z6pk%RRn#3Ylmpx>MXYExg>&7a!0K=3D%yx65O?L41qb zwJrnb2JBTgm&0lyak^jde6=xqyotv-9D7{D9gH2(_Y)U}DQR;aJ1D>&f?*gVHH;wm@U31CW z=@`sY@%+9gCy4aG|k8!%|r0E21c(aDOpGng@#xji&s+>b^Was_OjvoO=Wbi|hzS5o8pC;sk;$2}(%_Fd>8`7+NTe zlbIVbGMR}p6G)0xKrC(`2q?BFDvCwag1dl-OI;~gSFEjCm)dHzC@P{@f8XbvbMM?c za|77l`+nZf`-gIK=gyt8JnQ#)&ht#Yv3FZ`rM!B*l(QFk-u=cJEZ%KKaVI&c*NnD1 z)bw64+U`=*{~mMxa+Lh|ae!0%)ZI66_r)3V?q=M5P~Dxq1t^icxl(N5H$ANG{t3@+ zUZSqA!~Gw&sp~)D{tk8dFy7H#e(@z^!QF%8)jnRnRK4sLqkOi!)x3qzkr#L4`R?-S zb))UFW$GnQAh7jLZ+Z7gTy~XLdyNG*sE_v;3x1=nHu3Ux>dikde^cFBV=U!&%#`2Q zCd4}NpXaeI+v$pS@ekP7_sKb8PxG+4_?qwQ_!55!gnhel)~yjDnmdfMDt2`5#O^XS z8S4hozTAVWeF+Le-HoeN@#nEin{o9Dr>l}>n8nGEzs55DDZd_XC|<_9;GdhtD}%1u zg$;(D!(}b8+by`b_Zi$<+=umhTg%D5Z}VUCRtCEhtNN^((UVxzYt`k~xcs3&&gr6G z?QKoW+VHEw`|+|j`AeX|uQ4n3N8Ut40BCbFkDtZaB24toalgkyi*dEm_j?TfLq7iL zf(M4RVOR3Ultg=j|58fH<%lFMK_r(A=@?EimN#ZE&o|haW5&IYHx3;0m4UyO0W!oh zjP>s{3*vr2r&&M4l5Q1C^1q(G5SDZ6DZhl~r!gyubn1Ov$-+MyyNxILUM%rnc!0EU zXZ98kJj)m580>alb;g+X><(V_4~~DwpYc!04-?KH9Su7{Jcr2R!=yUb;c|TkTrM`) z8(5LY)rN1twSSp>A}%w zAIE-eTmt>umtAgL@~Y(ad&N`O)AyroSneL+`J`yC<3puN{e%xyDs`!N8*`zd?dNYB zU$!32*>9!ZgGgK?7M}T$Z`t&2ti$}qew=;9*Qf;xZ9%+1?%xJ%@Q&Nm^gCRUsdK2xN6oJUiLZr zXx{BkuPHx{wLiV4-_w2AA#OZqU=1D=3&qmCd})-khs5w-uONV#JuFt{>dX9D^rMUU zv?;403YQqIA*MMi?D0A`d$N(a4U;%~6|l_`;}(2x10Sn?YN2sCDECp^s)A`p#QLv(FrGBN!$%j#$6p6Fe#^L5^3!pwXt~xn{u7?0 zl!%!n3pq)*8Pk<;yAOu2T(AfDpA32N9)Ao$m<@e7JNphqgbFwtbiDlMtmAtQGT6JY zFJ2m0$=O=t3*)lc7jfs%@?j`Sjg{vDBN9@8Ima5P1!sQ>-IxLjGK(* zA$fVLQT#=%yt*DAuTW3EYP9|4Ds}lG-ufTKl7GkLA~l!~aQPc`c{{%Ln7Vv{9#CI< z1V6i7T|SD-)#`z#@xZl8K|O=ZYt-Fmad|+0?|EGBQMVt&_uf&j*p0iJ)a54p+~U4+ zz%S$Oay`v8G&p7$L1sWaQl^At%xARYoSF9m_!2T$|s>080Oi^Y5GSKrHbZ6!rEarAzk zG}!%O^qs~kaANsrWvl(o__jaT?x9tn$u$sJ^j^;Xge!V6XRC2_JXuV0w(xFpgmsXK zYL<6k3U{i@jkvrvUw*z56MkFW-Hv(PuGZ;pJn)|z)$MQa#Ff8O*E{e?5Ba_7Edbi( zRm~ROR$d&&gZjByYmiy-3a#J0hJyA6{%V8s4PSD5cFKQsgk zR<=d7`+6I|XK=0P@cX5F15%XOq?~7XP7YhkFH+Op!!J^^Eq!0J#@P3H@2Bv_(?fMT z;Vb+VE(fVa&R*h8>d^;{1G)Q(?|`>?Ne{*Q3;D!(mLXEn7ib+zx)!1lFG2`V{ZaI>4Hkz)oPh*6}jdkv(=B{^MCpb~5|wE#8Bj!WO;F0U5Ei zf5T7oVULX(&Q60-+@1N@w%LAmIy<-^pPj*8{s4pS&(?gzQ)~cS_X3v3mh8n>&SYoy zU}v$H@8##QLG1BRe|9$8^D%G7{A~a4@OeJlHMxupVFy0Nj~1}+2X*g5R^ zhxzGj7(jvq8_piaPmW-Z@b0XLy;@Mf&Sn4Fk8hmE_H-G}Mzfb5z}$brp1K~>7{fLd z_h!ZH>fVf%u)oag!N#&JISE$ER+c8%c(&*O9-6?OGs;+ief2dT&&r^c%GgBqS9c-GwYT3=pcq>-NHZ3=zECO-Roy}!)%UC^IxDv}3W3Q}& zU&P|<4_9GK4eW=NMg_YF1l*q`z-j$hiaqi&#@ERH++VPH?5#){o6pwg2UruFqW`~+qEMUtoVC-Uc*r;Q_V)sQmvrE{XxSw6hcCW`?ErbcO8t=NCvmb82BTG5kJd(k? zV{gBPwFFMGed2?e`ZJYVo>>kc;2+anX#XkKQzqSP?BqZ{Euv|Xn)8P2C9Rr6kB%M9< zF#J;X5TK%)Kv@V4Jh7h-XGj_NvP(Pm1ZU3{N7<8{z2AETKn->si0Em~4(5!2Rl!!? zV$6ew$PV`Ov)!D%@jS-62X8opwRj1z!C{cppE&zqgYg3yE9`p!d(hKt?PBcB>lo3* z5fD)9wvC|dw_)dO0&Tp**>_i9J>J8>b6WvEVf%ce0GO~VuQn#J5AkPH51?4=gJnF# z{=wPn%Z+wGn&93r_6aZ^o`8?do?Jm|@)@>i759S(fNxENb_gWnqL z-}f1yx^)KIC^yewA7dhL_t;X}Bme^ox&qW=|K1C^vzZ*baquA6Jsqx~ApY)|?HB^gv$>SJA(ag-vDZO#b7Ti zHj2Sd?1fEW!uJfeEw>ETD!bfQ#y$kn)KtL!X|NsQOdw(G;Z?j2%)qwZjpaZ%^l<<4 z0F|*@0o_Jbk5?a#iw zxI0^TCR_Lyq?oc-2C-KL*0Ck|Y{^V~x%atZ_io-h)Jx%SQFPYzaSo`-1-Oe8kUu>k;GZogZ`mUB5$mbw$CA1BZQc zlQI0`?Z${lj8Ql5G|v5KKR@r8E*c zTQ8~j{1BgZ`44>7&4Xs&u+gZwHaE2L^xB=v_?(Y;-7d(iE4K0aZ7cXiA3b3tKE2yW zJ^M1B_h#hMwd=uti|@dMzTJnE*-bN6@y$;g_w(-tKF^={8bo|W&wueX+n~iCy&GKg z;Cg=Q6|K2wHK434Z>Ax756CN*l^}57*gpY>D$bu)o<4R^Bt)xF6vy0;6P?3U*FD}` ziG+I`Z$O`;R$}yk@_1s7KPj{DYpu$t{ty)fYD)R>p4!M9^x>+BnvNWO+@_C<@~T>k z<05F?bG+z)x)dQZN-6*G##9?|d?YpxC6)4X#MA-h7l!BhFRV@YBlsHCC8YN!QG_7j zw=1XxVhxRWVfLd|(W9|uGL(ok;N(bATal;A_>7InqZO!IR9ILWiG{1{%~V}HTwRBg z98t46)!bmV6K4%5kHr00oK&;F5sfV9$5Hksfs-x%v1TnAH>6OU0p+z#%%{ERuFk^9 zf-r@JW_=`u8V-pL;)GOudLkIY;Sf3Z6GUq@!;YfO95W@G|8){wtuhaVg(*7xK;4=; zi*W2~GN5qL$I9ZI58>)X_ zA5eNdxc4e>;Z2C1&lHW=Gobc!fX|B_9V9+Y-$^w?Xps*N@#>rNHN zV;f@On&v*DUwI@p7e9w)MYNutThLeh6vLn*E|M-M1sjUgpHb8!^;e=sb8L>jlS#NG z)M#of4tJFYQ=vNNLzO1+ zQ$!Z}$w~2WV>CXcXaEV6XcRKXj8VarJW-%ujT2-3;^vfD+*n(i{bpjVDbXP9Ddf{Y z(K?7~&<&|GMUOPeDw0v1WzKR>&zgQP#3}T*Z<1Q;BKJGfyP-eN*o*&PpU$fqEP9~oA#~1|Shzesr#ioSa-skQ z^piuR&@LqjRv>y=3e?_eExd|`iky1Li(=}ic#dcXIbRk`$Zri3BlT8GUea_k%fY@D zDP`XpE>4OD>ubV6UoG^G))*yqjj_4D!RL$+z2eEjLeOvu=R4~s#ZgE<4goSh7)gy3 zZR&!_x)RibDipnxh%P||vv@s}BWZR>FsQ#GG1fV@Y&miv3K>}waZ$4CgHfV2RYo(D z=Ze@gLE`<8U*}w+#&%rT6DA4&naqtB2Kh;0h~v*C;d|(6d+l~*q_Mf zp`~IZ0PsmMY06AKl_x)o9JeQcl3jAXA%VWfp*T7uOO+G|7nO)X1Ij~n{)^(Mp%tG~ zgL-=Y5Q)1;2&PWN9{@3+=II>u?!3U*V^h(2-a2|pJegGOGm1(@XAnsh1QsNM8J46n zPP8E+iZn!<$BSg+L3Qf*U zP88j=#FY~ub|tH-{Kw6@YLXa`Avj37q>T%0>5!1A5UpXJ4IVONGO4ViZc(5zM9SFl z;4y646p$I}>4wdz;*?NCvClYl#<;K$%SHx>tdcoRjCZb*Mc-a_Y>f%U@2^#F@l%QF zc#?>%823SNmEzP);}DBUmF}Vu=$4szf$5@MBnj&z5+<5IUtHkz%Tg6MiQmEVdUT|D zVFZJ8l>tnHjZ0lS->mTmll3TnNG4KI6)mt!S$c0u_A{^+ZRO#!uO@*qQCo>NafWC| zOQ=`lr=n9#9&av33p6s?lKDRql^MUCRt=T)P{-0tYgw6ltB;wq$XPQ=E9tzdS)wia z;6V_E8$+p|i@~myN|?1#=rEY-QF9Kg^4J_D<*2hY8In%IP$JuEDugPkv+1xfF&H54so9iRLt`5zSRVaA8?7D>)EFjAzM6B4xHX5Etg%%Twj7D{R86uMvoaZor7bCmZ` zLY`%vXq_;zb!J3d>Rc`tV@7L-Ug)2M2DadiX7W(0j{P=vWLFS56 zvnGKpg+4EBLc7J-95X6Lc}&z{QmHrFrhwb(tE$h?c6#GuB0t+BP22qB6@<)cse-EG zC_C2}jmAZHwe931PLA1@Zi6@;3XPWYA|VFkl_f-p8Zf!{^XJd^YmS}(5~8)F`C1=Q zNd-SP6+&imV+s-*TY#={kaS7mCoP@nhg&q5cCLLEk)*_VdNW&Ci)?anGf$&v6OOBC z&9kj{rX-1g z9T30*+Mlr(ixahmkgjmD(zcL(C31*WNY^AU5$$SBm>h~;E)}N@Xu+7Ut%9P3TpSOz zHx_lF7jeywc0#^i@!o(LXcNAPtkEC0yFH z)`DNo#c=~>=2iU~B~1VsB~4h(GhlWu<)W)b8Dg@aNwu`IuE4i7qC``?5{Oiiz=W$X zSdURanO@C>FN*}n#Gr;G+5)khL~Y7Y>Kc?L88EB3q{L&dmvM1s);n4z_?!3Js(JK`6@2*v2;IzOY^E$b8(T!58IH39&wQtq8aXUAkfnN442*QSg#>c zfx&t`7p-CHHBYX^3FyvH&dt@e3DZPf&FU!1Uam!L8p7o&uw*x4g-uyU#{3Nzz0%J{ zVSiL7&;@*+S>2FA9-|nUwT#*~HX$~Bj_Zk$&|J9<%62dl>u6kg%N6nYw_*~O<0JKSX^ap}Ny$aG;rAQ{`#AV{8#tNg zS$hj&^hQFfZs%f{laKUhB*Nv5W?~g2jCltq$yBwG!_(69fypirR;>{3Jw_SnTxYCy)V=% zlCGDf=g?j(2#rk04?N1dxj4lds#Fomx%HReZIv+WitYg=!3l*87)srX&4r;OeIZ)e z-%GB^q8VtTl1M|HnegFi2APNI&!XZ$UfC85H3?{GE67%Xs%cv&c^`f(lB_g?D35UK>f&SL!l1 z4|DO00f7vo1NMqGgKXWKYyhf*##?brq^_RY)YPLwQ34gHLz$0em@JQQaeB*SCvgX5 zOFW7-p>^HH35X>R1;&F!k06#|MkM`}jWq0MMKZkKf>Msey=PG zln~34<>`yb9FbuIyaPK>T?2rYT=^twTqmkg!8|l~ue4NvSrgv>5^klTRbYz69cBwvA~wgM0V#7GiSsK1I|Cfic}@jCvPYpIPl zxVQm%<RoWV{eaR{S~IDD(cicjd*t0xOEx7Qk6QcYvv*Qidjz4q$Hhs~ zhIRsh;8dlZ-sfUshDl{Z>SJn29ZER8XjXBCkz_xmkr4IR2hb2C5o|+H1wB6mC1Ou! z#K|ZAyE4V4T@Fq35f>dS!jQWDAF!VAFpd46^jTvp9d`Ky`VYd(+a?$nOlmn>PP%=X zMqB?y`V?>&MUOs(SfcS^{#cIBxEOi_i&;lZ)l%Bf5h&r`sJ-S?l$NGh;sbpXA^#lowPUkIgv%v%>+Lmr(beK+HGtODSnJ ze@{+Sfb_n?w;*ZBiTN6r0|I4%vb?}S?0PX;!$X|~8v+Phr{i)p$@*{LS+p=L)I9`@ z0uL*k)_{jN1p7 zJw_{$Pf%DGh94pStWM(Z#fBYVbK(fjsmO5_U1o?55)hnbUW5QfD!Igfn`{{`mm5ke z(zd}&ojA48Y(z66GA$LH_-jLSwA6?W1uZp1>$!lZVyP<((Jm<;9y14V#w+ERj$kaw z;2@@U72c2}K^VOngRzC!HHPTptwYHulA1HQ%n+wxtk8}WaZg((lGEILh4@@W0u4Ci%pf8Va zWz+jwLtJgyG4lP24IDqH{V4z?STIsV5I>HOU7|z2L5pw4Gue04uQ-RJpSclPNok6c zB$I~4&PNVT@;8P!QCge`F{0SC%sH8lf2&jR-x^}v(PKHf6QPyCW9Kvx6g3S<8)SU* zI`DCIbpzF5t*Z{rqY&y%24RJABaS&op=Cn1vlp~xE4tYbXSNIkhe(LRnuRfq;lZO? zZ;0z$`A6x z4s4nmZOPw=Tb7YYP+`bda3>^uP$7pV$&Mt76`z{suQ4@PVEC?zuC;rz38IF;rV;>F zcNqkc1UBPaL~9o{B75qW%6gwFuPvi7aW}>f=@@|g7!wWy(?_iL9^4BBli*dt=@S7G zm-iZ?M_ENc@fPrudK3~S5G)w|Jw9>D1ihtYKp{viNnHX!X{#Z+n$chb%CD1qfQ6CJ z)_wS`G6#@gHbqkRqsnwbskR3UnKebpD3)q>@T{6Yw9kY124+EVj)x2}9L(u0565R$ zTlSIBNp_=|c-Rm{EpNKo?{O;F>LbGiVULuI8LE#S!BoF}!A2(!7Bom&6qVk6D92=qcd>2R%j31^1rY9r`&U_L(qu=lpG^GV%z&nJ0sIAU1 ziA1pZX~Qe+QLSQijb+wq;N=fQbDjYDGX@1?($ZNaO#q@%33O`A_&6hg^el8RR?H)% zO`?U*LE^jY1j37fyH&#T2lK#^cpmc%$E8XB0_Y&~;TH{pM_U?J0R6IjB?Lf(>5`0> z-B>KQ;Y6ukBwxv&{38}RNsq&W-vi4lFs-mKFm11q<=7pAJ);$6mcFQZi56Ltn|u$( z2KWORs(&(sF{|ulLmWp5JFgfb;1mFwE00C{XA|?Qus>zCp-O;z{RsS1^@br%4n>IA z2#mqcLg(1I6fgw#fdnBH-h@Hy6fqiIBl82MAo%_*K(!a=3}BUb$O z`N~yl)ZmdrhMb+>q*_#j^799eEEqm~ME=``80ht8aF9$`Mw&tby-aJ(P;)41zGH|H z9t$5p!+v6;3*qv{qe<95aDkKlNe#)+`LP=6JZpxf(viOL7oZ3t zJo^n=<&s^6IG$9p%E$Q<3qX47D=F*==^gqtl&`d(>~w^KAe&->D+xYAs=3CDfrQKt zCc=kcl4U`F!K98VL=uU3;#;LM$?yHn@XY4`NoubcS27h??O&#aQFRy+BCT{9JmDH- zI!e#mT6vWWU?#p-=D?9nFa5g;bx{&nnTQlZK|72SLUnPVRCxD1^ovaVV8~pk%0{Wn z(Ep;VrXxi&STKBT2|6=`A{sZH9|2APkYw%+LiBXy*++v{$VD(#)J!!*QJn2e;MxgmyakC2kp`Q{ zjM|d<5JnZRy{ffns}#O(NPhm>)}mvE9ly6VMf8)D7W-vu(IypFLhJk1qPJQl9MMP+ z)B%}Jod*$ut_@LE{h}Pv*SYA~P#mDdt8&N!Z-@fwUXx=vGcxvDSm=N%Hs**^N)S>5 zrJ7+x*%}H9gJjE9TV~#t9FgZ{Q-_#zpHA@&{wzo0b210?V2Nce_^5~%#vV3FoY8*>)TUwtvE06LVM9J9^&LbT9iz`**-^~30+Vjzn+X2M zcOV*_;(~T2?vd;$&x80%3DqiE+mRyngsn?)GWfNQ2+BExu2wnOck%)scXT7q`q8|= zvM!=C@=#Swo_2-8ZqIZPy^lP)Jzd1fX|<&@W=IVCDrI8QmpF)%{#=U5@seK4GRvd&EL@-=^p`{5IEZMkquEL;mV?KaE=GblhVt|zoMr&i58o- zxz6;fK0xYl2^7J-Jw-q5bz8@LwCkO)0;&)h4e#|NfD>7;QUv9H)>HJ&G}g0DFCE0m zA4_3PK~}gN#959PyBG#b-;H+Iql7d;P#OwP|=+*jm!yC2d=Thdo7 z^D^rKj#=pC`S0#Hm&0S7^)!^(O2|mjhQ4kHU42RBj<_&%d4519LEP0B(Iq%?u(+P@ z3uO(~wZ)^_>(}~<6T*=MxYZH_ulH3Fh)zU8+B>lWwCE#@aN%i`3Y>nEvX|A7AIEJg zmEo6@X$eVZ9Kysq;d#oCbB29~ z6Kpbkvhs950pNC|Munc}ZyV;s`~>PLghE4mmY3uGA*u#Q)#0&mi0qMu*x#Q{G}`*~ zP=C=~ZjBtXc9H41!~Injqb&}zRiskkiuP_Cs3S(iej5jh9`dDFU7$ow^gd8%R*d07RZPth>OhVNUSm5NT4U~ zn~6gMMVkg~n|(J>N?)uRPPbB?HQL;LKM-akk)@ow!>Ylnv>!MD*a!|i`tYAL0#**n zI9%%DzjcGsr%UmUq)Zi!CvF}jPDl@*e(df+B1dK+QjXy#gG7H9`4BLbPWSeemW-WH zN}O45UYjp^c_qb3VRd7^Xrq#1qtV~wi(Jcq%f{*Q*XMhuznveGk%$D2bRm*#CBhX4 zljc(egs(U>H`168Z;k~?K9D1W%o0plT|s(3T_D;3gOH}js|8L_Ri)SbfcHs3LbRbU z0hyeX1hlE`_38#Veln3aVkWl?lL@!d^2n4V4-BJ|=8$g>4ioL=h7xl?J? ze(Sc8GP5#s;7TakoM*?u^JZ=yDNclDO*SJ@zTQ%FsvY=0Mv87ONQ2^K@C?2gDNb{d z5mG%$OLTBpMzaa`yKGboT9piojiVqvXpr7Q87#GB6vdqpYOr19{KrOVW|#BOwkRC{ zL9}Chhi!r5=%G~{ zddBrdioiG@KlTr^Ly_6vb?Q|8skQY*pN|%2yY?juclLqNltP2R1O9n%w3J&(6C`|1 zu{bF$JIa!Aou99y?#f~@ob;V{1ani5)(oA>h`8$ak6Vzl3XTRzDV-kVsDpa z=GNl0vpp^vnNyci_AQR{mIBSC)$Y_KP#6ILOOUIpNMy(JmD~AIF#yC6H&YmqMN>V!;dCmgk-0 zMBCKhf>LEuES?}1bND>!VVg7lcL}iBX|hR8o%Rn@x(;(!ZJHq3g7b73_t6Pr7zyLd zsu;w@I7b=*T7Xk3GR|e4ozQgf*aXoY?~$er@T~ep6LI3f7ECVER+O&*k>q_7#n~+` zwH3^in`ueKw?Uh>JaW%6{1sMn#^FO zaE!e8Plf1R4oPLD{8)+=cnn4HaV~M!WZ`!)ljd;{iFK;WH+^sc{v0z0hYtwMSvr-% zyTw;b6~|MCWV|tT{Z!G4ikzgjP8I#KxEjWm1oP9g+dfqU+&g01YAw31e$wTHp<+IB zodD{tsjyLGJ(SWWkWKZZ>HDXu!1EDEY}s@>BgN(8W#^2LK2Y-AvR~yFl>c$}bUF@B z>qmO~zdS$&PvPt7IPyYy?$&Xga*KYq*~&lY{M zL}x%UQ6lM^5T}ImqE+LOC8%lIz+F#7fK1$y8mi)KLG%KSuszW7balMLW?B z0cV52_TliOhY*uGVr}{TiK5pp5L2{XB(dZli^@tek=h-Obmzl@r>&*;$d)bo@&X$u zl@pZ_8_BQN>b-#j2%@P_Y|*(nsDId;=amMt|h45YEIpmP!v`=Hnr8 z>JbJ`8NSbkD045p%l`$=0otq;I8A9sM#;f(l$JvNK4ond-BK&^T}nhQ){$X^FKR_E zw-KhxI!(s`f?Fd5j6{QpIp&)Y(LNqC{mNxVxI7{Tx_&Fe=S;hwbbMz?RP=DT9P}YF zvB{)f8ikcu<}`n`M7%F5Mtd13HFVU}E_Yz3$hpcMXzBHSl&U|-rI#y_7E2cG$lKCv zSQUrpZ-}UbY)Kz`;u<0+o|lDq%&?Q?KZ>K%jB{s|E^=(Ohb`112TEn&`a-6|q1UBl z<>7t>#i=FW_gX?Mca78aE7{J8OE9?KNe)yC!*Wbpt%Gw*^)K2U&Ax=_AdeMvgq>3vaL5Cju+yb8cxVOxM66W>=qH|gn>5%2Y z1)>j#+-=ADRe-^Qa1JdHo#0y8fCPmLzO{Z=j^ClAm5g&9Q2F-aBc)PG@uj@Vt(PF4 zl@9NO;?1x%ez-(Tc5<36?f?6uN2p2K;!%iz_B~1SD}OegK%s2W!xeg_ynP8GpcRds zpNM57Jj#R3^QusnQRLX`j6%E~urvZc#E~$+=b9*HotggSG~ZC$5#-~HL8_F64Y+@l zu%cyjR-qLIOjkNuD9UYG_N~Ft-LYN}gOY(p@C`IF4eBLTVflJhN6&*=|5@ZjII~lAsQRW*C934eN z1W6BRbCh3l1xfit8#X^mA%#i&#z$`eLngV8ntXWfwZD#;0D`B9W1!>UbU-zizsnSzI2Hb_O& z%F>qKo}nyMXBR!462QvtfLu1w6-ab5)5r7a(74nnC||2FY$9K9kAes_y`#erkz8Za zL>ci6>ya!v3LQ8x2pLL9A0tOcDVaHbzB-zza>r3b*aH~XV46CEgfYmt3~K6%vn-;- z_z+q__PNYJNLt4p2tcVy(XDlhW9If*p9{}&?-s7eEN+j=j{wVq%Gj2@BCPOJRGTwPuk%=FFD;lx zW~8=^EErL9&IlA^R$|2hYf$W8pp*xK-A9R0q1OU(sJtIcN<<%3o5k@`nMUHMqc29a zKo*v{l0-c$sYlP;Sv(q5BRb>%RcABY+Dn51N|oxL9qHc!!~l4O2p z@+!Q$71Fa4DfDg;9bAPL&?-apX@OaYPT&{?o6%EFoQ#+TJW20bNa!{uPSAN)@`rr+ zZ{U_ne2{b^IYODDs9-NfxCB;~@rJC7%=&eDVbLr^oYIn#N9mAg6zwXUh#-<5k%^#0 z8aflCof@_KX$;e+E}bF)o!!#qbL1c?RiTu^8AWJIB8Ex|%NFAn8fzst$Zr76g;I|v zP@W#HgJzYG)|BY3iF3^@V;Ax_BoKoxA_AqPJ(b?LXk!z~$l-WeeGN)4q%YI54IQ+U z{!`V@(JMkJ53Gi?-DP}h0)kx1WW+A#Yiq5sjbte0Q&Xth|okVX24AH62ygZ?@APTXQ z6!Hy0j}9l+p!^s#lMyEq>+55Hbnw|ZxC3WPtrPN<&p3|b? zMncCf|4F|+5U0gb1WY8RukB%i7iGW9BIy9RRlRcTo@=3{J zMrt-CJE=ol>hBq54F$4My_@RsA|(ng2S}S9L5C#K=V;aR&`QO54Bah`RLQF+@Q5Qr z&ynkmjvwMgi#n9Lu)QzzA`t^xrhS#MVSxc?%_PooZ9Umq@Ea?^6{c-)`AW!_l=mzJ zzUTnNQd*8EmcjyHB_^0rq*|Z}2~LYpWEVm=REJ(6TA)Z0K)NBilZdV!1oIW{pP2q4 zWJ0?51zJ6M1H{(u*kJ{g0b(Dv!?0CFUWYF*hUJXnVig*K zRHxXcRdj?oieZ^S|LCaYjegoLF`6iefh`z}8k*BZtH@>Q-(XbI-jwTgbx6HJ^d7f~A>_E!~PXhCxpC{HTQ zos5r;VNSOlbFkvyYU)mC-KUNfqMMW$>$UkU1k5bThM@rxqmmLjzXuJd#A);*tql|e zZ_^CcOM;(hS_aPbFiR|~1|7^qKYIfmUWhAc%QqNZgPg==uN5w^(hxCdeI-svi+z&t z=z8Xj0Uq;iqX*QH=7>%A4EBwo%-o>-$$ReV^y*v^D~9yP;*#5TCp7|DrD^e_#3UNM2}1%Pk2crgbr0{cf)ci z(XkDugdE{&zf8#o?Gp1Q(CSN!_v95zU0ZZy>*q|U%N7>sPj;-UFnr`Nm#P>svSw&# z1p18~#Y4qmWAv62gJq0oc{(fi8qMkCDN$txWLlIGOiIbP ziG;VUq#G$IeS^`H4OvGv8lj}1u1JD*Y2wTlDLdPoM|V8$%tQ%39`Rs3Ifv>Kr3$Z!Ebpp6JLmis@5W)PuMI)_3t&;SikpYl4;K}|;7 zJa+|6{nVLdSD+Y_tKv0I_XO?xybT^3(aivUvY=h3!85`|#V~k+p%G{}r{Y<*R53$n zIfr6bPO+(RDs-K5T5&K*(SS~LO$#`L2UK-Zw2^b2dL_3NjpI~p4NEhVR_)u0iB{96 mjVUPw7BsfB`26uWrX8plHyNjbXN;LvahzyXI&IqIY5xOi-_%n8 diff --git a/36354ee63d9240659b46ca78579a5c64.jfr b/36354ee63d9240659b46ca78579a5c64.jfr deleted file mode 100644 index 52073814033e0c800a042c1454ac52f2e27617e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53647 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 G+WWt8T);H| diff --git a/sentry/test88-20250408-152005.jfr b/sentry/test88-20250408-152005.jfr deleted file mode 100644 index 76e629fcb7f6d0f84b26f155629b6d4ba4d18ede..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57839 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 diff --git a/sentry/test88-20250408-152146.jfr b/sentry/test88-20250408-152146.jfr deleted file mode 100644 index 54296763239be7392ff1367f10366a86915c11bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85992 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 Date: Fri, 26 Sep 2025 20:07:10 +0200 Subject: [PATCH 19/31] Support ProfileLifecycle.TRACE (#4576) * delete unused JfrFrame and JfrToSentryProfileconverter * use passed-in profilingTracesHz parameter instead of hardcoded value * start profiler before starting the transaction when ProfileLifecycle.TRACE is used to have the profile ID when SentryTracer is created * use improved way to calculate timestamp of sample * api dump * let profile-lifecycle be set from external_options, add tests for SpringBoot autoconfig * initialize stackTraceFactory only once per chunk * rename profile data classes, add deserialization and tests * extract methods in ProfileConverter, fix SentryProfile serialization and make fields private * use wall=[interval] instead of setting the event to wall and setting the interval separately, this seems to work better and create more samples * start/stop profiler in OtelSentrySpanProcesser in trace mode for root spans * add profiler dependency to jakarta-opentelemetry sample, add needed configs * add dependenies and config to spring-boot-jakarta sample * remove connection status check * extract event visitor * Add enum for ProfileChunk platform * fallback to default temp directory for profiling on jvm if directory is not configured * cleanup some minor things * remove ProfilingInitializer, fix comments * Format code * add getter/setter to sample and metadata * fix compile error * add comment/todo for deleteOnExit * Profiling - Deduplication and cleanup (#4681) * add readme and info about commit of the source repository * delete jfr file on jvm exit * further split into smaller methods * deduplicate frames in order to save bandwidth, add converter tests * remove Platform Enum, use string constants instead for compatibility with cross platform frameworks * implement equals and hashcode for SentryStackFrame to make frame deduplication work * bump api * improve error handling, fix start stop start flow * add new testfile * calculate ticksPerNanosecond in constructor * adapt Ratelimiter to check for both ProfileChunk and ProfileChunkUi ratelimiting * update ratelimiter test to check for both profileChunk and profileChunkUi drops * use string constant instead of string * Format code * add non aggregating event collector to send each event individually, deduplicate stacks * adapt converter tests to new non-aggregated converter * Format code * add logging to loadProfileConverter * Format code * fix duplication of events * catch all exception happening when converting from jfr * add exists and writable info to log message * add method to safely delete file * remove setNative call * fix test * fix reference to commit we vendored from * drop event if it cannot be processed to not lose the whole chunk * make format * fix test * Format code * Profiling - OTEL profiling fix, Stabilization, Logging (#4746) * add skipProfiling flag to TransactionOptions to be able to skip profiling and handle cases where profiling has been started by otel * add profilerId to spanContext so that otel span processor can propagate this to the exporter and SentryTracer * immediately end profiling when stopProfiler is called * bump api, fix android api 24 code * catch all exception happening when converting from jfr * simplify JavaContinuous profiler by catching AsyncProfiler instantiation exceptions in provider * add exists and writable info to log message * add method to safely delete file * remove setNative call * fix test * fix reference to commit we vendored from * drop event if it cannot be processed to not lose the whole chunk * Format code * fix test * Format code * fix test * catch exceptions in startProfiler/stopProfiler * fallback to threadId -1 if it cannot be resolved --------- Co-authored-by: Sentry Github Bot --------- Co-authored-by: Sentry Github Bot --------- Co-authored-by: Sentry Github Bot --- .../core/AndroidContinuousProfiler.java | 2 +- .../api/sentry-async-profiler.api | 11 +- ...AsyncProfilerToSentryProfileConverter.java | 344 +++++++++++------ .../convert/NonAggregatingEventCollector.java | 37 ++ .../profiling/JavaContinuousProfiler.java | 140 ++++--- ...yncProfilerContinuousProfilerProvider.java | 17 +- .../vendor/asyncprofiler/README.md | 4 + .../vendor/asyncprofiler/jfr/JfrReader.java | 2 + ...yncProfilerToSentryProfileConverterTest.kt | 302 +++++++++++++++ .../JavaContinuousProfilerTest.kt | 87 ++--- .../resources/async_profiler_test_sample.jfr | Bin 0 -> 380882 bytes .../OtelSentrySpanProcessor.java | 22 ++ .../build.gradle.kts | 8 + .../build.gradle.kts | 1 + .../boot/jakarta/ProfilingInitializer.java | 25 -- .../boot/jakarta/SentryDemoApplication.java | 6 - .../src/main/resources/application.properties | 4 +- .../jakarta/SentryAutoConfigurationTest.kt | 9 + .../boot/SentryAutoConfigurationTest.kt | 9 + sentry/api/sentry.api | 94 +++-- .../main/java/io/sentry/ExternalOptions.java | 14 + .../src/main/java/io/sentry/ProfileChunk.java | 5 +- sentry/src/main/java/io/sentry/Scopes.java | 23 +- sentry/src/main/java/io/sentry/Sentry.java | 48 ++- .../java/io/sentry/SentryEnvelopeItem.java | 4 +- .../main/java/io/sentry/SentryOptions.java | 4 + .../src/main/java/io/sentry/SentryTracer.java | 18 +- .../src/main/java/io/sentry/SpanContext.java | 14 + .../JavaContinuousProfilerProvider.java | 2 +- .../profiling/ProfilingServiceLoader.java | 9 +- .../io/sentry/protocol/SentryStackFrame.java | 58 +++ .../sentry/protocol/profiling/JfrFrame.java | 69 ---- .../JfrToSentryProfileConverter.java | 356 ------------------ .../protocol/profiling/SentryProfile.java | 171 ++++++--- .../{JfrSample.java => SentrySample.java} | 63 +++- ...etadata.java => SentryThreadMetadata.java} | 50 ++- .../java/io/sentry/transport/RateLimiter.java | 39 +- .../java/io/sentry/ExternalOptionsTest.kt | 7 + .../test/java/io/sentry/JsonSerializerTest.kt | 4 +- sentry/src/test/java/io/sentry/ScopesTest.kt | 28 ++ .../test/java/io/sentry/SentryClientTest.kt | 2 +- .../java/io/sentry/SentryEnvelopeItemTest.kt | 12 +- .../test/java/io/sentry/SentryOptionsTest.kt | 2 + .../SentryProfileSerializationTest.kt | 114 ++++++ .../io/sentry/transport/RateLimiterTest.kt | 28 +- .../test/resources/json/sentry_profile.json | 63 ++++ 46 files changed, 1452 insertions(+), 879 deletions(-) create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md create mode 100644 sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt rename sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/{ => profiling}/JavaContinuousProfilerTest.kt (87%) create mode 100644 sentry-async-profiler/src/test/resources/async_profiler_test_sample.jfr delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/ProfilingInitializer.java delete mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrFrame.java delete mode 100644 sentry/src/main/java/io/sentry/protocol/profiling/JfrToSentryProfileConverter.java rename sentry/src/main/java/io/sentry/protocol/profiling/{JfrSample.java => SentrySample.java} (50%) rename sentry/src/main/java/io/sentry/protocol/profiling/{ThreadMetadata.java => SentryThreadMetadata.java} (50%) create mode 100644 sentry/src/test/java/io/sentry/protocol/SentryProfileSerializationTest.kt create mode 100644 sentry/src/test/resources/json/sentry_profile.json 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..654cd17fe44 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..af2962cbd61 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -4,10 +4,19 @@ 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;Lio/sentry/ILogger;)V public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; } +public final class io/sentry/asyncprofiler/convert/NonAggregatingEventCollector : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector { + public fun ()V + public fun afterChunk ()V + public fun beforeChunk ()V + public fun collect (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event;)V + public fun finish ()Z + public fun forEach (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector$Visitor;)V +} + public final class io/sentry/asyncprofiler/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V public fun close (Z)V 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..4489497e815 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,152 +1,57 @@ package io.sentry.asyncprofiler.convert; +import io.sentry.DateUtils; +import io.sentry.ILogger; import io.sentry.Sentry; +import io.sentry.SentryLevel; import io.sentry.SentryStackTraceFactory; import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments; import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.JfrConverter; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector; import io.sentry.protocol.SentryStackFrame; -import io.sentry.protocol.profiling.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 java.util.Map; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { + private static final double NANOS_PER_SECOND = 1_000_000_000.0; + private static final long UNKNOWN_THREAD_ID = -1; + private final @NotNull SentryProfile sentryProfile = new SentryProfile(); + private final @NotNull SentryStackTraceFactory stackTraceFactory; + private final @NotNull ILogger logger; + private final @NotNull Map frameDeduplicationMap = new HashMap<>(); + private final @NotNull Map, Integer> stackDeduplicationMap = new HashMap<>(); - public JfrAsyncProfilerToSentryProfileConverter(JfrReader jfr, Arguments args) { + public JfrAsyncProfilerToSentryProfileConverter( + JfrReader jfr, + Arguments args, + @NotNull SentryStackTraceFactory stackTraceFactory, + @NotNull ILogger logger) { super(jfr, args); + this.stackTraceFactory = stackTraceFactory; + this.logger = logger; } @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)); + } + + @Override + protected EventCollector createCollector(Arguments args) { + return new NonAggregatingEventCollector(); } public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath) @@ -155,15 +60,206 @@ public void visit(Event event, long value) { try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { Arguments args = new Arguments(); args.cpu = false; + args.wall = true; args.alloc = false; args.threads = true; args.lines = true; args.dot = true; - converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args); + SentryStackTraceFactory stackTraceFactory = + new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions()); + ILogger logger = Sentry.getGlobalScope().getOptions().getLogger(); + converter = + new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory, logger); converter.convert(); } return converter.sentryProfile; } + + private class ProfileEventVisitor implements EventCollector.Visitor { + private final @NotNull SentryProfile sentryProfile; + private final @NotNull SentryStackTraceFactory stackTraceFactory; + private final @NotNull JfrReader jfr; + private final @NotNull Arguments args; + private final double ticksPerNanosecond; + + public ProfileEventVisitor( + @NotNull SentryProfile sentryProfile, + @NotNull SentryStackTraceFactory stackTraceFactory, + @NotNull JfrReader jfr, + @NotNull Arguments args) { + this.sentryProfile = sentryProfile; + this.stackTraceFactory = stackTraceFactory; + this.jfr = jfr; + this.args = args; + ticksPerNanosecond = jfr.ticksPerSec / NANOS_PER_SECOND; + } + + @Override + public void visit(Event event, long samples, long value) { + try { + StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + long threadId = resolveThreadId(event.tid); + + if (stackTrace != null) { + if (args.threads) { + processThreadMetadata(event, threadId); + } + + processSampleWithStack(event, threadId, stackTrace); + } + } catch (Exception e) { + logger.log(SentryLevel.WARNING, "Failed to process JFR event " + event, e); + } + } + + private long resolveThreadId(int eventId) { + Long javaThreadId = jfr.javaThreads.get(eventId); + return javaThreadId != null ? javaThreadId : UNKNOWN_THREAD_ID; + } + + private void processThreadMetadata(Event event, long threadId) { + if (threadId == UNKNOWN_THREAD_ID) { + return; + } + + 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 processSampleWithStack(Event event, long threadId, StackTrace stackTrace) { + int stackIndex = addStackTrace(stackTrace); + + SentrySample sample = new SentrySample(); + sample.setTimestamp(calculateTimestamp(event)); + sample.setThreadId(String.valueOf(threadId)); + sample.setStackId(stackIndex); + + sentryProfile.getSamples().add(sample); + } + + private double calculateTimestamp(Event event) { + long nanosFromStart = (long) ((event.time - jfr.chunkStartTicks) / ticksPerNanosecond); + + long timeNs = jfr.chunkStartNanos + nanosFromStart; + + return DateUtils.nanosToSeconds(timeNs); + } + + private int addStackTrace(StackTrace stackTrace) { + List callStack = createFramesAndCallStack(stackTrace); + + Integer existingIndex = stackDeduplicationMap.get(callStack); + if (existingIndex != null) { + return existingIndex; + } + + int stackIndex = sentryProfile.getStacks().size(); + sentryProfile.getStacks().add(callStack); + stackDeduplicationMap.put(callStack, stackIndex); + return stackIndex; + } + + private List createFramesAndCallStack(StackTrace stackTrace) { + List callStack = new ArrayList<>(); + + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; + int[] locations = stackTrace.locations; + + for (int i = 0; i < methods.length; i++) { + StackTraceElement element = getStackTraceElement(methods[i], types[i], locations[i]); + if (element.isNativeMethod() || isNativeFrame(types[i])) { + continue; + } + + SentryStackFrame frame = createStackFrame(element); + int frameIndex = getOrAddFrame(frame); + callStack.add(frameIndex); + } + + return callStack; + } + + // Get existing frame index or add new frame and return its index + private int getOrAddFrame(SentryStackFrame frame) { + Integer existingIndex = frameDeduplicationMap.get(frame); + + if (existingIndex != null) { + return existingIndex; + } + + int newIndex = sentryProfile.getFrames().size(); + sentryProfile.getFrames().add(frame); + frameDeduplicationMap.put(frame, newIndex); + return newIndex; + } + + 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 boolean shouldMarkAsSystemFrame(StackTraceElement element, String className) { + return element.isNativeMethod() || className.isEmpty(); + } + + private @Nullable Integer extractLineNumber(StackTraceElement element) { + return element.getLineNumber() != 0 ? element.getLineNumber() : null; + } + } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java new file mode 100644 index 00000000000..ce9dcaebf36 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java @@ -0,0 +1,37 @@ +package io.sentry.asyncprofiler.convert; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.Event; +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector; +import java.util.ArrayList; +import java.util.List; + +public final class NonAggregatingEventCollector implements EventCollector { + final List events = new ArrayList<>(); + + @Override + public void collect(Event e) { + events.add(e); + } + + @Override + public void beforeChunk() { + // No-op + } + + @Override + public void afterChunk() { + // No-op + } + + @Override + public boolean finish() { + return false; + } + + @Override + public void forEach(Visitor visitor) { + for (Event event : events) { + visitor.visit(event, event.samples(), event.value()); + } + } +} 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..6b45d568394 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; @@ -25,7 +24,6 @@ import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.SentryRandom; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -53,15 +51,13 @@ public final class JavaContinuousProfiler private @Nullable Future stopFuture; private final @NotNull List payloadBuilders = new ArrayList<>(); private @NotNull SentryId profilerId = SentryId.EMPTY_ID; - private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); private @NotNull String filename = ""; - private final @NotNull AsyncProfiler profiler; + private @NotNull AsyncProfiler profiler; private volatile boolean shouldSample = true; - private boolean shouldStop = false; private boolean isSampled = false; private int rootSpanCounter = 0; @@ -72,26 +68,47 @@ public JavaContinuousProfiler( final @NotNull ILogger logger, final @Nullable String profilingTracesDirPath, final int profilingTracesHz, - final @NotNull ISentryExecutorService executorService) { + final @NotNull ISentryExecutorService executorService) + throws Exception { this.logger = logger; this.profilingTracesDirPath = profilingTracesDirPath; this.profilingTracesHz = profilingTracesHz; this.executorService = executorService; + initializeProfiler(); + } + + private void initializeProfiler() throws Exception { this.profiler = AsyncProfiler.getInstance(); + // Check version to verify profiler is working + String version = profiler.execute("version"); + logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version); } private boolean init() { - // We initialize it only once if (isInitialized) { return true; } isInitialized = true; + if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, "Disabling profiling because no profiling traces dir path is defined in options."); return false; } + + File profileDir = new File(profilingTracesDirPath); + + if (!profileDir.canWrite() || !profileDir.exists()) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)", + profilingTracesDirPath, + profileDir.canWrite(), + profileDir.exists()); + return false; + } + if (profilingTracesHz <= 0) { logger.log( SentryLevel.WARNING, @@ -102,7 +119,6 @@ private boolean init() { return true; } - @SuppressWarnings("ReferenceEquality") @Override public void startProfiler( final @NotNull ProfileLifecycle profileLifecycle, @@ -142,13 +158,14 @@ public void startProfiler( logger.log(SentryLevel.DEBUG, "Started Profiler."); start(); } + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error starting profiler: ", e); } } 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) { @@ -161,7 +178,6 @@ private void initScopes() { private void start() { initScopes(); - // Let's initialize trace folder and profiling interval if (!init()) { return; } @@ -177,33 +193,30 @@ 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(); } + filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr"; - String startData = null; + + File jfrFile = new File(filename); + try { final String profilingIntervalMicros = String.format("%dus", (int) SECONDS.toMicros(1) / profilingTracesHz); + // Example command: start,jfr,event=wall,interval=9900us,file=/path/to/trace.jfr final String command = String.format( "start,jfr,event=wall,interval=%s,file=%s", profilingIntervalMicros, filename); - System.out.println(command); - startData = profiler.execute(command); + + profiler.execute(command); + } catch (Exception e) { logger.log(SentryLevel.ERROR, "Failed to start profiling: ", e); - } - // check if profiling started - if (startData == null) { + filename = ""; + // Try to clean up the file if it was created + safelyRemoveFile(jfrFile); return; } @@ -213,10 +226,6 @@ private void start() { profilerId = new SentryId(); } - if (chunkId == SentryId.EMPTY_ID) { - chunkId = new SentryId(); - } - try { stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } catch (RejectedExecutionException e) { @@ -224,7 +233,8 @@ private void start() { SentryLevel.ERROR, "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", e); - shouldStop = true; + // If we can't schedule the auto-stop, stop immediately without restart + stop(false); } } @@ -243,10 +253,12 @@ public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { if (rootSpanCounter < 0) { rootSpanCounter = 0; } - shouldStop = true; + // Stop immediately without restart + stop(false); break; case MANUAL: - shouldStop = true; + // Stop immediately without restart + stop(false); break; } } @@ -260,50 +272,55 @@ private void stop(final boolean restartProfiler) { // check if profiler was created and it's running if (!isRunning) { // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the - // ids + // id profilerId = SentryId.EMPTY_ID; - chunkId = SentryId.EMPTY_ID; return; } - String endData = null; + File jfrFile = new File(filename); + try { - endData = profiler.execute("stop,jfr"); - } catch (IOException e) { - throw new RuntimeException(e); + profiler.execute("stop,jfr"); + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error stopping profiler, attempting cleanup: ", e); + // Clean up file if it exists + safelyRemoveFile(jfrFile); } - // check if profiler end successfully - if (endData == null) { - logger.log( - SentryLevel.ERROR, - "An error occurred while collecting a profile chunk, and it won't be sent."); - } else { - // The scopes can be null if the profiler is started before the SDK is initialized (app - // 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. + // The scopes can be null if the profiler is started before the SDK is initialized (app + // 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. + if (jfrFile.exists() && jfrFile.canRead() && jfrFile.length() > 0) { try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) { + jfrFile.deleteOnExit(); payloadBuilders.add( new ProfileChunk.Builder( profilerId, - chunkId, + new SentryId(), new HashMap<>(), - new File(filename), + jfrFile, startProfileChunkTimestamp, - "java")); + ProfileChunk.PLATFORM_JAVA)); } + } else { + logger.log( + SentryLevel.WARNING, + "JFR file is invalid or empty: exists=%b, readable=%b, size=%d", + jfrFile.exists(), + jfrFile.canRead(), + jfrFile.length()); + safelyRemoveFile(jfrFile); } + // Always clean up state, even if stop failed isRunning = false; - // A chunk is finished. Next chunk will have a different id. - chunkId = SentryId.EMPTY_ID; filename = ""; if (scopes != null) { sendChunks(scopes, scopes.getOptions()); } - if (restartProfiler && !shouldStop) { + if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); start(); } else { @@ -311,6 +328,8 @@ private void stop(final boolean restartProfiler) { profilerId = SentryId.EMPTY_ID; logger.log(SentryLevel.DEBUG, "Profile chunk finished."); } + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error stopping profiler: ", e); } } @@ -323,9 +342,8 @@ public void reevaluateSampling() { public void close(final boolean isTerminating) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { rootSpanCounter = 0; - shouldStop = true; + stop(false); if (isTerminating) { - stop(false); isClosed.set(true); } } @@ -363,9 +381,21 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti } } + private void safelyRemoveFile(File file) { + try { + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + logger.log(SentryLevel.INFO, "Failed to remove jfr file %s.", file.getAbsolutePath(), e); + } + } + @Override public boolean isRunning() { - return isRunning; + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + return isRunning && !filename.isEmpty(); + } } @VisibleForTesting 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..226cfc09084 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 @@ -3,6 +3,8 @@ import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.SentryLevel; import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler; import io.sentry.profiling.JavaContinuousProfilerProvider; import io.sentry.profiling.JavaProfileConverterProvider; @@ -22,10 +24,15 @@ public final class AsyncProfilerContinuousProfilerProvider String profilingTracesDirPath, int profilingTracesHz, ISentryExecutorService executorService) { - return new JavaContinuousProfiler( - logger, - profilingTracesDirPath, - 10, // default profilingTracesHz - executorService); + try { + return new JavaContinuousProfiler( + logger, profilingTracesDirPath, profilingTracesHz, executorService); + } catch (Exception e) { + logger.log( + SentryLevel.WARNING, + "Failed to initialize AsyncProfiler. Profiling will be disabled.", + e); + return NoOpContinuousProfiler.getInstance(); + } } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md new file mode 100644 index 00000000000..733a69f1c3b --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md @@ -0,0 +1,4 @@ +# Vendored AsyncProfiler code for converting JFR Files +- Vendored-in from commit https://github.com/async-profiler/async-profiler/tree/fe1bc66d4b6181413847f6bbe5c0db805f3e9194 +- Only the code related to JFR conversion is included. +- The `AsyncProfiler` itself is included as a dependency in the Maven project. diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java index abc9a0024b4..98c8aa01b22 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java @@ -60,6 +60,8 @@ public final class JfrReader implements Closeable { public final Dictionary types = new Dictionary<>(); public final Map typesByName = new HashMap<>(); public final Dictionary threads = new Dictionary<>(); + // Maps thread IDs to Java thread IDs + // Change compared to original async-profiler JFR reader public final Dictionary javaThreads = new Dictionary<>(); public final Dictionary classes = new Dictionary<>(); public final Dictionary strings = new Dictionary<>(); 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..2b9c8ae1104 --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt @@ -0,0 +1,302 @@ +package io.sentry.asyncprofiler.convert + +import io.sentry.DateUtils +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.SentryStackTraceFactory +import io.sentry.TracesSampler +import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider +import io.sentry.protocol.profiling.SentryProfile +import io.sentry.test.DeferredExecutorService +import java.io.IOException +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit +import java.util.* +import kotlin.io.path.Path +import kotlin.math.absoluteValue +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +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 mockStackTraceFactory = mock() + + val scopes: IScopes = mock() + val scope: IScope = mock() + + val options = + spy(SentryOptions()).apply { + dsn = mockDsn + profilesSampleRate = 1.0 + isDebug = true + setLogger(mockLogger) + // Set in-app packages for testing + addInAppInclude("io.sentry") + addInAppInclude("com.example") + } + + init { + whenever(mockTracesSampler.sampleSessionProfile(any())).thenReturn(true) + // Setup default in-app behavior for stack trace factory + whenever(mockStackTraceFactory.isInApp(any())).thenAnswer { invocation -> + val className = invocation.getArgument(0) + className.startsWith("io.sentry") || className.startsWith("com.example") + } + } + + 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) + + // Ensure the global scope returns proper options for the static converter method + whenever(fixture.scope.options).thenReturn(fixture.options) + } + + @AfterTest + fun clear() { + Sentry.close() + fixture.mockedSentry.close() + } + + @Test + fun `check number of samples for specific frame`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + val tracingFilterFrame = + sentryProfile.frames.filter { + it.function == "slowFunction" && it.module == "io.sentry.samples.console.Main" + } + + val tracingFilterFrameIndexes = tracingFilterFrame.map { sentryProfile.frames.indexOf(it) } + val tracingFilterStacks = + sentryProfile.stacks.filter { it.any { inner -> tracingFilterFrameIndexes.contains(inner) } } + val tracingFilterStackIds = tracingFilterStacks.map { sentryProfile.stacks.indexOf(it) } + val tracingFilterSamples = + sentryProfile.samples.filter { tracingFilterStackIds.contains(it.stackId) } + + // Sample size base on 101 samples/sec and 5 sec of profiling + // So expected around 500 samples (with some margin) + assertTrue( + tracingFilterSamples.count() >= 500 && tracingFilterSamples.count() <= 600, + "Expected sample count between 500 and 600, but was ${tracingFilterSamples.count()}", + ) + } + + @Test + fun `check number of samples for specific thread`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + val mainThread = + sentryProfile.threadMetadata.entries.firstOrNull { it.value.name == "main" }?.key + + val samples = sentryProfile.samples.filter { it.threadId == mainThread } + + // Sample size base on 101 samples/sec and 5 sec of profiling + // So expected around 500 samples (with some margin) + assertTrue( + samples.count() >= 500 && samples.count() <= 600, + "Expected sample count between 500 and 600, but was ${samples.count()}", + ) + } + + @Test + fun `check no duplicate frames`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + val frameSet = sentryProfile.frames.toSet() + + assertEquals(frameSet.size, sentryProfile.frames.size) + } + + @Test + fun `convertFromFile with valid JFR returns populated SentryProfile`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + assertNotNull(sentryProfile) + assertValidSentryProfile(sentryProfile) + } + + @Test + fun `convertFromFile parses timestamps correctly`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + val samples = sentryProfile.samples + assertTrue(samples.isNotEmpty()) + + val minTimestamp = samples.minOf { it.timestamp } + val maxTimestamp = samples.maxOf { it.timestamp } + val sampleTimeStamp = + DateUtils.nanosToDate((maxTimestamp * 1000 * 1000 * 1000).toLong()).toInstant() + + // The sample was recorded around "2025-09-05T08:14:50" in UTC timezone + val referenceTimestamp = LocalDateTime.parse("2025-09-05T08:14:50").toInstant(ZoneOffset.UTC) + val between = ChronoUnit.MILLIS.between(sampleTimeStamp, referenceTimestamp).absoluteValue + + assertTrue(between < 5000, "Sample timestamp should be within 5s of reference timestamp") + assertTrue(maxTimestamp >= minTimestamp, "Max timestamp should be >= min timestamp") + assertTrue( + maxTimestamp - minTimestamp <= 10, + "There should be a max difference of <10s between min and max timestamp", + ) + } + + @Test + fun `convertFromFile extracts thread metadata correctly`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + val threadMetadata = sentryProfile.threadMetadata + val samples = sentryProfile.samples + + assertTrue(threadMetadata.isNotEmpty()) + + // Verify thread IDs in samples match thread metadata keys + val threadIdsFromSamples = samples.map { it.threadId }.toSet() + threadIdsFromSamples.forEach { threadId -> + assertTrue( + threadMetadata.containsKey(threadId), + "Thread metadata should contain thread ID: $threadId", + ) + } + + // Verify thread metadata has proper values + threadMetadata.forEach { (_, metadata) -> + assertNotNull(metadata.name, "Thread name should not be null") + assertEquals(0, metadata.priority, "Thread priority should be default (0)") + } + } + + @Test + fun `converter processes frames with complete information`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + val frames = sentryProfile.frames + assertTrue(frames.isNotEmpty()) + + // Find frames with complete information + val completeFrames = + frames.filter { frame -> + frame.function != null && + frame.module != null && + frame.lineno != null && + frame.filename != null + } + + assertTrue(completeFrames.isNotEmpty(), "Should have frames with complete information") + } + + @Test + fun `converter marks in-app frames correctly`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + val frames = sentryProfile.frames + + // Verify system packages are marked as not in-app + val systemFrames = + frames.filter { frame -> + frame.module?.let { + it.startsWith("java.") || it.startsWith("sun.") || it.startsWith("jdk.") + } ?: false + } + + val inappSentryFrames = + frames.filter { frame -> frame.module?.startsWith("io.sentry.") ?: false } + + val emptyModuleFrames = frames.filter { it.module.isNullOrEmpty() } + + // Verify system classes are not marked as in-app + systemFrames.forEach { frame -> + assertFalse(frame.isInApp ?: false, "System classes should not be marked as in app") + } + + // Verify sentry classes are marked as in-app + inappSentryFrames.forEach { frame -> + assertTrue(frame.isInApp ?: false, "Sentry classes should be marked as in app") + } + + // Verify empty class names are marked as not in-app + emptyModuleFrames.forEach { frame -> + assertFalse(frame.isInApp ?: true, "Empty module frame should not be in-app") + } + } + + @Test + fun `converter filters native methods`() { + val file = Path(loadFile("async_profiler_test_sample.jfr")) + + val sentryProfile = fixture.getSut()!!.convertFromFile(file) + + // Native methods should be filtered out during frame creation + // Verify no frames have characteristics of native methods + sentryProfile.frames.forEach { frame -> + // Native methods would have been skipped, so no frame should indicate native + assertTrue( + frame.filename?.isNotEmpty() == true || frame?.module?.isNotEmpty() == true, + "Frame should have some non-native information", + ) + } + } + + @Test(expected = IOException::class) + fun `convertFromFile with non-existent file throws IOException`() { + val nonExistentFile = Path("/non/existent/file.jfr") + + JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(nonExistentFile) + } + + private fun loadFile(path: String): String = javaClass.classLoader!!.getResource(path)!!.file + + private fun assertValidSentryProfile(profile: SentryProfile) { + assertNotNull(profile.samples, "Samples should not be null") + assertNotNull(profile.frames, "Frames should not be null") + assertNotNull(profile.stacks, "Stacks should not be null") + assertNotNull(profile.threadMetadata, "Thread metadata should not be null") + } +} 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 87% 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..86f5d51fee2 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,8 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -import kotlin.use +import org.mockito.ArgumentMatchers.startsWith 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 +38,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() @@ -120,9 +117,6 @@ class JavaContinuousProfilerTest { assertTrue(profiler.isRunning) // We are scheduling the profiler to stop at the end of the chunk, so it should still be running profiler.stopProfiler(ProfileLifecycle.MANUAL) - assertTrue(profiler.isRunning) - // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart - fixture.executor.runAll() assertFalse(profiler.isRunning) } @@ -278,28 +272,19 @@ class JavaContinuousProfilerTest { fixture.executor.runAll() // We assert that no trace files are written assertTrue(File(fixture.options.profilingTracesDirPath!!).list()!!.isEmpty()) - verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Failed to start profiling: "), any()) + val expectedPath = fixture.options.profilingTracesDirPath + verify(fixture.mockLogger) + .log( + eq(SentryLevel.WARNING), + eq( + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)" + ), + eq(expectedPath), + eq(false), + eq(true), + ) } - // @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() @@ -337,28 +322,42 @@ class JavaContinuousProfilerTest { assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) fixture.executor.runAll() + // At this point the chunk has been submitted to the executor, but yet to be sent verify(fixture.scopes, never()).captureProfileChunk(any()) profiler.stopProfiler(ProfileLifecycle.MANUAL) - // We stop the profiler, which should send a chunk + // We stop the profiler, which should send both the first and last chunk fixture.executor.runAll() - verify(fixture.scopes).captureProfileChunk(any()) + verify(fixture.scopes, times(2)).captureProfileChunk(any()) } @Test fun `close without terminating stops all profiles after chunk is finished`() { val profiler = fixture.getSut() - profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler) assertTrue(profiler.isRunning) - // We are scheduling the profiler to stop at the end of the chunk, so it should still be running + // We are closing the profiler, which should stop all profiles after the chunk is finished profiler.close(false) - assertTrue(profiler.isRunning) + assertFalse(profiler.isRunning) // However, close() already resets the rootSpanCounter assertEquals(0, profiler.rootSpanCounter) + } + + @Test + fun `profiler can be stopped and restarted`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) - // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart + profiler.stopProfiler(ProfileLifecycle.MANUAL) fixture.executor.runAll() assertFalse(profiler.isRunning) + + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + fixture.executor.runAll() + + assertTrue(profiler.isRunning) + verify(fixture.mockLogger, never()) + .log(eq(SentryLevel.WARNING), startsWith("JFR file is invalid or empty"), any(), any(), any()) } @Test @@ -407,24 +406,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-async-profiler/src/test/resources/async_profiler_test_sample.jfr b/sentry-async-profiler/src/test/resources/async_profiler_test_sample.jfr new file mode 100644 index 0000000000000000000000000000000000000000..35b4c768eac9e2d1abd469a6017ec753af4366ad GIT binary patch literal 380882 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

Mj)7;JQ0BU(!d!rSb(%c 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 G-2Vq8b~(rZ literal 0 HcmV?d00001 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..bb374f4a517 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); @@ -105,6 +117,9 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri sentryParentSpanId, baggage); sentrySpan.getSpanContext().setOrigin(SentrySpanExporter.TRACE_ORIGIN); + sentrySpan + .getSpanContext() + .setProfilerId(scopes.getOptions().getContinuousProfiler().getProfilerId()); spanStorage.storeSentrySpan(spanContext, sentrySpan); } @@ -159,6 +174,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 40eb4c04c2b..0eeaf30d2bd 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 b3593ed46bb..cde2de9c0f4 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 6c424c12321..fde201015f3 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 @@ -12,6 +12,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 @@ -40,6 +41,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 @@ -202,6 +204,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) @@ -255,6 +260,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 43331e8a267..1d80b3b6480 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 @@ -12,6 +12,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 @@ -39,6 +40,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 @@ -201,6 +203,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) @@ -254,6 +259,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 cac3a6bae7d..6968129b4ee 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 @@ -1952,6 +1954,8 @@ public final class io/sentry/PerformanceCollectionData { } public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field PLATFORM_ANDROID Ljava/lang/String; + public static final field PLATFORM_JAVA Ljava/lang/String; 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 equals (Ljava/lang/Object;)Z @@ -4021,6 +4025,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun getOrigin ()Ljava/lang/String; public fun getParentSpanId ()Lio/sentry/SpanId; public fun getProfileSampled ()Ljava/lang/Boolean; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getSampled ()Ljava/lang/Boolean; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanId ()Lio/sentry/SpanId; @@ -4035,6 +4040,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setOperation (Ljava/lang/String;)V public fun setOrigin (Ljava/lang/String;)V + public fun setProfilerId (Lio/sentry/protocol/SentryId;)V public fun setSampled (Ljava/lang/Boolean;)V public fun setSampled (Ljava/lang/Boolean;Ljava/lang/Boolean;)V public fun setSamplingDecision (Lio/sentry/TracesSamplingDecision;)V @@ -5952,6 +5958,7 @@ public final class io/sentry/protocol/SentrySpan$JsonKeys { public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun equals (Ljava/lang/Object;)Z public fun getAbsPath ()Ljava/lang/String; public fun getAddrMode ()Ljava/lang/String; public fun getColno ()Ljava/lang/Integer; @@ -5973,6 +5980,7 @@ public final class io/sentry/protocol/SentryStackFrame : io/sentry/JsonSerializa public fun getSymbolAddr ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getVars ()Ljava/util/Map; + public fun hashCode ()I public fun isInApp ()Ljava/lang/Boolean; public fun isNative ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V @@ -6308,91 +6316,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 62daabc26de..f35e01af797 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; } @@ -548,4 +554,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..0aa6b7e524d 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -20,6 +20,9 @@ @ApiStatus.Internal public final class ProfileChunk implements JsonUnknown, JsonSerializable { + public static final String PLATFORM_ANDROID = "android"; + public static final String PLATFORM_JAVA = "java"; + private @Nullable DebugMeta debugMeta; private @NotNull SentryId profilerId; private @NotNull SentryId chunkId; @@ -47,7 +50,7 @@ public ProfileChunk() { new File("dummy"), new HashMap<>(), 0.0, - "android", + PLATFORM_ANDROID, SentryOptions.empty()); } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index ce11b19f738..12309e355c5 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -947,6 +947,20 @@ 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 unless skipProfiling is + // true in TransactionOptions. + // 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() + && getOptions().isContinuousProfilingEnabled() + && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE + && transactionContext.getProfilerId().equals(SentryId.EMPTY_ID)) { + getOptions() + .getContinuousProfiler() + .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); + } + transaction = spanFactory.createTransaction( transactionContext, this, transactionOptions, compositePerformanceCollector); @@ -969,15 +983,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 b213bbb9798..db566cbe1a6 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; @@ -678,25 +680,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..0c9b07f7cd0 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.equals(profileChunk.getPlatform())) { final IProfileConverter profileConverter = ProfilingServiceLoader.loadProfileConverter(); if (profileConverter != null) { @@ -303,7 +303,7 @@ private static void ensureAttachmentSizeLimit( final SentryProfile profile = profileConverter.convertFromFile(traceFile.toPath()); profileChunk.setSentryProfile(profile); - } catch (IOException e) { + } catch (Exception e) { throw new SentryEnvelopeException("Profile conversion failed"); } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 5c35c4e830d..f868ecacad2 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3366,6 +3366,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/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 0496f407219..21d5088a181 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -83,8 +83,8 @@ public SentryTracer( setDefaultSpanData(root); - final @NotNull SentryId continuousProfilerId = - scopes.getOptions().getContinuousProfiler().getProfilerId(); + final @NotNull SentryId continuousProfilerId = getProfilerId(); + if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) { this.contexts.setProfile(new ProfileContext(continuousProfilerId)); } @@ -229,7 +229,7 @@ public void finish( } }); - // any un-finished childs will remain unfinished + // any un-finished children will remain unfinished // as relay takes care of setting the end-timestamp + deadline_exceeded // see // https://github.com/getsentry/relay/blob/40697d0a1c54e5e7ad8d183fc7f9543b94fe3839/relay-general/src/store/transactions/processor.rs#L374-L378 @@ -244,7 +244,8 @@ public void finish( .onTransactionFinish(this, performanceCollectionData.get(), scopes.getOptions()); } if (scopes.getOptions().isContinuousProfilingEnabled() - && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE + && root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID)) { scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE); } if (performanceCollectionData.get() != null) { @@ -543,8 +544,7 @@ private ISpan createChild( /** Sets the default data in the span, including profiler _id, thread id and thread name */ private void setDefaultSpanData(final @NotNull ISpan span) { final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); - final @NotNull SentryId profilerId = - scopes.getOptions().getContinuousProfiler().getProfilerId(); + final @NotNull SentryId profilerId = getProfilerId(); if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) { span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); } @@ -553,6 +553,12 @@ private void setDefaultSpanData(final @NotNull ISpan span) { span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); } + private @NotNull SentryId getProfilerId() { + return !root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID) + ? root.getSpanContext().getProfilerId() + : scopes.getOptions().getContinuousProfiler().getProfilerId(); + } + @Override public @NotNull ISpan startChild(final @NotNull String operation) { return this.startChild(operation, (String) null); diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 2999ea4a2b8..8bfc83e6458 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -55,6 +55,12 @@ public class SpanContext implements JsonUnknown, JsonSerializable { protected @Nullable Baggage baggage; + /** + * Set the profiler id associated with this transaction. If set to a non-empty id, this value will + * be sent to sentry instead of {@link SentryOptions#getContinuousProfiler} + */ + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + public SpanContext( final @NotNull String operation, final @Nullable TracesSamplingDecision samplingDecision) { this(new SentryId(), new SpanId(), operation, null, samplingDecision); @@ -304,6 +310,14 @@ public int hashCode() { return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus()); } + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + public void setProfilerId(@NotNull SentryId profilerId) { + this.profilerId = profilerId; + } + // region JsonSerializable public static final class JsonKeys { 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/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index ec09de6075f..ef5a9373c42 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -5,6 +5,7 @@ import io.sentry.IProfileConverter; import io.sentry.ISentryExecutorService; import io.sentry.NoOpContinuousProfiler; +import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import java.util.Iterator; import java.util.ServiceLoader; @@ -49,16 +50,22 @@ public final class ProfilingServiceLoader { * @return an IProfileConverter instance or null if no provider is found */ public static @Nullable IProfileConverter loadProfileConverter() { + ILogger logger = ScopesAdapter.getInstance().getGlobalScope().getOptions().getLogger(); try { JavaProfileConverterProvider provider = loadSingleProvider(JavaProfileConverterProvider.class); if (provider != null) { + logger.log( + SentryLevel.DEBUG, + "Loaded profile converter from provider: %s", + provider.getClass().getName()); return provider.getProfileConverter(); } else { + logger.log(SentryLevel.DEBUG, "No profile converter provider found, returning null"); return null; } } catch (Throwable t) { - // Log error and return null to skip conversion + logger.log(SentryLevel.ERROR, "Failed to load profile converter provider, returning null", t); return null; } } diff --git a/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java b/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java index 2675813b52c..f2dfd961333 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryStackFrame.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -369,6 +370,63 @@ public static final class JsonKeys { public static final String VARS = "vars"; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SentryStackFrame that = (SentryStackFrame) o; + return Objects.equals(preContext, that.preContext) + && Objects.equals(postContext, that.postContext) + && Objects.equals(vars, that.vars) + && Objects.equals(framesOmitted, that.framesOmitted) + && Objects.equals(filename, that.filename) + && Objects.equals(function, that.function) + && Objects.equals(module, that.module) + && Objects.equals(lineno, that.lineno) + && Objects.equals(colno, that.colno) + && Objects.equals(absPath, that.absPath) + && Objects.equals(contextLine, that.contextLine) + && Objects.equals(inApp, that.inApp) + && Objects.equals(_package, that._package) + && Objects.equals(_native, that._native) + && Objects.equals(platform, that.platform) + && Objects.equals(imageAddr, that.imageAddr) + && Objects.equals(symbolAddr, that.symbolAddr) + && Objects.equals(instructionAddr, that.instructionAddr) + && Objects.equals(addrMode, that.addrMode) + && Objects.equals(symbol, that.symbol) + && Objects.equals(unknown, that.unknown) + && Objects.equals(rawFunction, that.rawFunction) + && Objects.equals(lock, that.lock); + } + + @Override + public int hashCode() { + return Objects.hash( + preContext, + postContext, + vars, + framesOmitted, + filename, + function, + module, + lineno, + colno, + absPath, + contextLine, + inApp, + _package, + _native, + platform, + imageAddr, + symbolAddr, + instructionAddr, + addrMode, + symbol, + unknown, + rawFunction, + lock); + } + @Override public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) throws IOException { 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/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 6c3460c8eb0..fc07e59c063 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -20,6 +20,8 @@ import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -171,8 +173,13 @@ private void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean r */ @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) private boolean isRetryAfter(final @NotNull String itemType) { - final DataCategory dataCategory = getCategoryFromItemType(itemType); - return isActiveForCategory(dataCategory); + final List dataCategory = getCategoryFromItemType(itemType); + for (DataCategory category : dataCategory) { + if (isActiveForCategory(category)) { + return true; + } + } + return false; } /** @@ -181,33 +188,33 @@ private boolean isRetryAfter(final @NotNull String itemType) { * @param itemType the item itemType (eg event, session, attachment, ...) * @return the DataCategory eg (DataCategory.Error, DataCategory.Session, DataCategory.Attachment) */ - private @NotNull DataCategory getCategoryFromItemType(final @NotNull String itemType) { + private @NotNull List getCategoryFromItemType(final @NotNull String itemType) { switch (itemType) { case "event": - return DataCategory.Error; + return Collections.singletonList(DataCategory.Error); case "session": - return DataCategory.Session; + return Collections.singletonList(DataCategory.Session); case "attachment": - return DataCategory.Attachment; + return Collections.singletonList(DataCategory.Attachment); case "profile": - return DataCategory.Profile; + return Collections.singletonList(DataCategory.Profile); // When we send a profile chunk, we have to check for profile_chunk_ui rate limiting, - // because that's what relay returns to rate limit Android. When (if) we will implement JVM - // profiling we will have to check both rate limits. + // because that's what relay returns to rate limit Android. + // And ProfileChunk rate limiting for JVM. case "profile_chunk": - return DataCategory.ProfileChunkUi; + return Arrays.asList(DataCategory.ProfileChunkUi, DataCategory.ProfileChunk); case "transaction": - return DataCategory.Transaction; + return Collections.singletonList(DataCategory.Transaction); case "check_in": - return DataCategory.Monitor; + return Collections.singletonList(DataCategory.Monitor); case "replay_video": - return DataCategory.Replay; + return Collections.singletonList(DataCategory.Replay); case "feedback": - return DataCategory.Feedback; + return Collections.singletonList(DataCategory.Feedback); case "log": - return DataCategory.LogItem; + return Collections.singletonList(DataCategory.LogItem); default: - return DataCategory.Unknown; + return Collections.singletonList(DataCategory.Unknown); } } 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..ba8b5507abb 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 c7637c4c7cb..22f689abac0 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..858ca43ab11 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -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 0c8f382fc53..6d0bcd5790b 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -411,6 +411,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() @@ -468,6 +469,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/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 5bea60408d3..ab997a26f76 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -456,7 +456,7 @@ class RateLimiterTest { } @Test - fun `drop profileChunk items as lost`() { + fun `drop profileChunkUi items as lost`() { val rateLimiter = fixture.getSUT() val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) @@ -481,6 +481,32 @@ class RateLimiterTest { verifyNoMoreInteractions(fixture.clientReportRecorder) } + @Test + fun `drop profileChunk items as lost`() { + val rateLimiter = fixture.getSUT() + + val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) + val attachmentItem = + SentryEnvelopeItem.fromAttachment( + fixture.serializer, + NoOpLogger.getInstance(), + Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), + 1000, + ) + val envelope = + SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(profileChunkItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:profile_chunk:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)) + .recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + @Test fun `drop feedback items as lost`() { val rateLimiter = fixture.getSUT() 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 + } + } +} From 1e998bde87a9e5c01705f5d790b4567217b2aa56 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 29 Sep 2025 14:32:56 +0200 Subject: [PATCH 20/31] change profiler api from Path to String to keep Android Api 21 compatibility --- .../api/sentry-async-profiler.api | 4 ++-- sentry-async-profiler/build.gradle.kts | 3 +-- ...AsyncProfilerToSentryProfileConverter.java | 5 ++--- ...AsyncProfilerProfileConverterProvider.java | 2 +- ...yncProfilerToSentryProfileConverterTest.kt | 21 +++++++++---------- sentry/api/sentry.api | 2 +- .../java/io/sentry/IProfileConverter.java | 3 +-- sentry/src/main/java/io/sentry/Sentry.java | 15 ++++++++----- .../java/io/sentry/SentryEnvelopeItem.java | 2 +- .../profiling/ProfilingServiceLoaderTest.kt | 3 +-- 10 files changed, 30 insertions(+), 30 deletions(-) diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index af2962cbd61..63bcf17906f 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -5,7 +5,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;Lio/sentry/SentryStackTraceFactory;Lio/sentry/ILogger;)V - public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; + public static fun convertFromFileStatic (Ljava/lang/String;)Lio/sentry/protocol/profiling/SentryProfile; } public final class io/sentry/asyncprofiler/convert/NonAggregatingEventCollector : io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector { @@ -41,7 +41,7 @@ public final class io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverte public final class io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider$AsyncProfilerProfileConverter : io/sentry/IProfileConverter { public fun ()V - public fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; + public fun convertFromFile (Ljava/lang/String;)Lio/sentry/protocol/profiling/SentryProfile; } public final class io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments { diff --git a/sentry-async-profiler/build.gradle.kts b/sentry-async-profiler/build.gradle.kts index 0f4628e3e06..5b78d5b99e4 100644 --- a/sentry-async-profiler/build.gradle.kts +++ b/sentry-async-profiler/build.gradle.kts @@ -12,8 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } kotlin { explicitApi() } 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 4489497e815..f56bf323c5f 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 @@ -16,7 +16,6 @@ import io.sentry.protocol.profiling.SentrySample; import io.sentry.protocol.profiling.SentryThreadMetadata; import java.io.IOException; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -54,10 +53,10 @@ protected EventCollector createCollector(Arguments args) { return new NonAggregatingEventCollector(); } - public static @NotNull SentryProfile convertFromFileStatic(@NotNull Path jfrFilePath) + public static @NotNull SentryProfile convertFromFileStatic(@NotNull String jfrFilePath) throws IOException { JfrAsyncProfilerToSentryProfileConverter converter; - try (JfrReader jfrReader = new JfrReader(jfrFilePath.toString())) { + try (JfrReader jfrReader = new JfrReader(jfrFilePath)) { Arguments args = new Arguments(); args.cpu = false; args.wall = true; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java index bb78af134e4..0e03996e46b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java @@ -25,7 +25,7 @@ public static final class AsyncProfilerProfileConverter implements IProfileConve @Override public @NotNull io.sentry.protocol.profiling.SentryProfile convertFromFile( - @NotNull java.nio.file.Path jfrFilePath) throws java.io.IOException { + @NotNull String jfrFilePath) throws java.io.IOException { return JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(jfrFilePath); } } 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 index 2b9c8ae1104..d565fd9d51d 100644 --- 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 @@ -17,7 +17,6 @@ import java.time.LocalDateTime import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.* -import kotlin.io.path.Path import kotlin.math.absoluteValue import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -95,7 +94,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `check number of samples for specific frame`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) val tracingFilterFrame = @@ -120,7 +119,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `check number of samples for specific thread`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) val mainThread = @@ -138,7 +137,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `check no duplicate frames`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) val frameSet = sentryProfile.frames.toSet() @@ -148,7 +147,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `convertFromFile with valid JFR returns populated SentryProfile`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -158,7 +157,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `convertFromFile parses timestamps correctly`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -184,7 +183,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `convertFromFile extracts thread metadata correctly`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -211,7 +210,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `converter processes frames with complete information`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -232,7 +231,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `converter marks in-app frames correctly`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -269,7 +268,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test fun `converter filters native methods`() { - val file = Path(loadFile("async_profiler_test_sample.jfr")) + val file = loadFile("async_profiler_test_sample.jfr") val sentryProfile = fixture.getSut()!!.convertFromFile(file) @@ -286,7 +285,7 @@ class JfrAsyncProfilerToSentryProfileConverterTest { @Test(expected = IOException::class) fun `convertFromFile with non-existent file throws IOException`() { - val nonExistentFile = Path("/non/existent/file.jfr") + val nonExistentFile = "/non/existent/file.jfr" JfrAsyncProfilerToSentryProfileConverter.convertFromFileStatic(nonExistentFile) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6968129b4ee..5f694bae511 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -827,7 +827,7 @@ public abstract interface class io/sentry/IPerformanceSnapshotCollector : io/sen } public abstract interface class io/sentry/IProfileConverter { - public abstract fun convertFromFile (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; + public abstract fun convertFromFile (Ljava/lang/String;)Lio/sentry/protocol/profiling/SentryProfile; } public abstract interface class io/sentry/IReplayApi { diff --git a/sentry/src/main/java/io/sentry/IProfileConverter.java b/sentry/src/main/java/io/sentry/IProfileConverter.java index 9b594c2dbf5..c47d9f2d0a5 100644 --- a/sentry/src/main/java/io/sentry/IProfileConverter.java +++ b/sentry/src/main/java/io/sentry/IProfileConverter.java @@ -2,7 +2,6 @@ import io.sentry.protocol.profiling.SentryProfile; import java.io.IOException; -import java.nio.file.Path; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -22,5 +21,5 @@ public interface IProfileConverter { * @throws IOException If an error occurs while reading or converting the file */ @NotNull - SentryProfile convertFromFile(@NotNull Path jfrFilePath) throws IOException; + SentryProfile convertFromFile(@NotNull String jfrFilePath) throws IOException; } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index db566cbe1a6..d149d3c8900 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -33,13 +33,11 @@ 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; @@ -698,8 +696,15 @@ private static void initJvmContinuousProfiling(@NotNull SentryOptions options) { try { String profilingTracesDirPath = options.getProfilingTracesDirPath(); if (profilingTracesDirPath == null) { - profilingTracesDirPath = - Files.createTempDirectory("profiling_traces").toAbsolutePath().toString(); + File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); + boolean createDirectorySuccess = tempDir.mkdirs(); + + if (!createDirectorySuccess) { + throw new IllegalArgumentException( + "Creating a fallback directory for profiling failed in " + + tempDir.getAbsolutePath()); + } + profilingTracesDirPath = tempDir.getAbsolutePath(); options.setProfilingTracesDirPath(profilingTracesDirPath); } @@ -711,7 +716,7 @@ private static void initJvmContinuousProfiling(@NotNull SentryOptions options) { options.getExecutorService()); options.setContinuousProfiler(continuousProfiler); - } catch (IOException e) { + } catch (Exception e) { options .getLogger() .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 0c9b07f7cd0..71f9961bd84 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -301,7 +301,7 @@ private static void ensureAttachmentSizeLimit( if (profileConverter != null) { try { final SentryProfile profile = - profileConverter.convertFromFile(traceFile.toPath()); + profileConverter.convertFromFile(traceFile.getAbsolutePath()); profileChunk.setSentryProfile(profile); } catch (Exception e) { throw new SentryEnvelopeException("Profile conversion failed"); diff --git a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt index a0963aec100..9bf1e5f601f 100644 --- a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt +++ b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt @@ -8,7 +8,6 @@ import io.sentry.ProfileLifecycle import io.sentry.TracesSampler import io.sentry.protocol.SentryId import io.sentry.protocol.profiling.SentryProfile -import java.nio.file.Path import kotlin.test.Test import org.mockito.kotlin.mock @@ -35,7 +34,7 @@ class JavaProfileConverterProviderStub : JavaProfileConverterProvider { } class ProfileConverterStub() : IProfileConverter { - override fun convertFromFile(jfrFilePath: Path): SentryProfile { + override fun convertFromFile(jfrFilePath: String): SentryProfile { TODO("Not yet implemented") } } From 0c93c2b1c6d71a0879c6e710d3727ec50c12615e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 29 Sep 2025 15:33:45 +0200 Subject: [PATCH 21/31] mark all profiling related classes as internal --- .../convert/JfrAsyncProfilerToSentryProfileConverter.java | 2 ++ .../asyncprofiler/convert/NonAggregatingEventCollector.java | 2 ++ .../provider/AsyncProfilerContinuousProfilerProvider.java | 2 ++ .../provider/AsyncProfilerProfileConverterProvider.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/convert/Arguments.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/convert/Classifier.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/convert/Frame.java | 2 ++ .../vendor/asyncprofiler/convert/JfrConverter.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java | 3 +++ .../asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java | 2 ++ .../sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java | 3 +++ .../asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java | 2 ++ .../asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java | 3 +++ .../asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java | 3 +++ .../vendor/asyncprofiler/jfr/event/AllocationSample.java | 3 +++ .../asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java | 2 ++ .../vendor/asyncprofiler/jfr/event/ContendedLock.java | 3 +++ .../asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java | 2 ++ .../vendor/asyncprofiler/jfr/event/EventAggregator.java | 2 ++ .../vendor/asyncprofiler/jfr/event/EventCollector.java | 3 +++ .../vendor/asyncprofiler/jfr/event/ExecutionSample.java | 3 +++ .../vendor/asyncprofiler/jfr/event/GCHeapSummary.java | 2 ++ .../vendor/asyncprofiler/jfr/event/LiveObject.java | 3 +++ .../vendor/asyncprofiler/jfr/event/MallocEvent.java | 3 +++ .../vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java | 2 ++ .../vendor/asyncprofiler/jfr/event/ObjectCount.java | 2 ++ .../io/sentry/profiling/JavaContinuousProfilerProvider.java | 2 ++ .../java/io/sentry/profiling/JavaProfileConverterProvider.java | 2 ++ .../main/java/io/sentry/profiling/ProfilingServiceLoader.java | 3 +++ 32 files changed, 75 insertions(+) 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 f56bf323c5f..f6db9a86ab3 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 @@ -20,9 +20,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { private static final double NANOS_PER_SECOND = 1_000_000_000.0; private static final long UNKNOWN_THREAD_ID = -1; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java index ce9dcaebf36..c39259b7799 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/NonAggregatingEventCollector.java @@ -4,7 +4,9 @@ import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event.EventCollector; import java.util.ArrayList; import java.util.List; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public final class NonAggregatingEventCollector implements EventCollector { final List events = new ArrayList<>(); 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 226cfc09084..de2ba455bc8 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 @@ -8,6 +8,7 @@ import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler; import io.sentry.profiling.JavaContinuousProfilerProvider; import io.sentry.profiling.JavaProfileConverterProvider; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -15,6 +16,7 @@ * JavaProfileConverterProvider}. This provider integrates AsyncProfiler with Sentry's continuous * profiling system and provides profile conversion functionality. */ +@ApiStatus.Internal public final class AsyncProfilerContinuousProfilerProvider implements JavaContinuousProfilerProvider { diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java index 0e03996e46b..b8aa9111fae 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java @@ -3,6 +3,7 @@ import io.sentry.IProfileConverter; import io.sentry.asyncprofiler.convert.JfrAsyncProfilerToSentryProfileConverter; import io.sentry.profiling.JavaProfileConverterProvider; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,6 +11,7 @@ * AsyncProfiler implementation of {@link JavaProfileConverterProvider}. This provider integrates * AsyncProfiler's JFR converter with Sentry's profiling system. */ +@ApiStatus.Internal public final class AsyncProfilerProfileConverterProvider implements JavaProfileConverterProvider { @Override diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java index d4d81600481..f3b44fccc09 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java @@ -9,9 +9,11 @@ import java.lang.reflect.Modifier; import java.util.*; import java.util.regex.Pattern; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class Arguments { public @NotNull String title = "Flame Graph"; public @Nullable String highlight; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java index 7990e1b3e72..ec955870930 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java @@ -8,9 +8,11 @@ import static io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Frame.*; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal abstract class Classifier { enum Category { diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java index d1426255458..bb9768442c4 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; import java.util.HashMap; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public final class Frame extends HashMap { private static final long serialVersionUID = 1L; public static final byte TYPE_INTERPRETED = 0; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java index 70f1747fac7..006bce7e03c 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter.java @@ -24,8 +24,10 @@ import java.nio.charset.StandardCharsets; import java.util.BitSet; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +@ApiStatus.Internal public abstract class JfrConverter extends Classifier { protected final @NotNull JfrReader jfr; protected final @NotNull Arguments args; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java index 7c7fc5d8bf0..1727dc2156c 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class ClassRef { public final long name; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java index 9c9ab8a873c..5f3191a37e6 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Arrays; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal /** Fast and compact long->Object map. */ public final class Dictionary { private static final int INITIAL_CAPACITY = 16; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java index f552f1fa819..83d3a2772e1 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Arrays; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal /** Fast and compact long->int map. */ public final class DictionaryInt { private static final int INITIAL_CAPACITY = 16; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java index 127ce9a6262..0c12292106a 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal abstract class Element { void addChild(Element e) {} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java index d5971b4802c..eb85be46eb3 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java @@ -8,9 +8,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class JfrClass extends Element { final int id; final boolean simpleType; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java index c71787f8379..635b87fd0f3 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java @@ -6,9 +6,11 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class JfrField extends Element { final @Nullable String name; final int type; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java index 98c8aa01b22..a0297d7afc1 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -86,6 +87,7 @@ public final class JfrReader implements Closeable { private int malloc; private int free; + @ApiStatus.Internal public JfrReader(String fileName) throws IOException { this.ch = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ); this.buf = ByteBuffer.allocateDirect(BUFFER_SIZE); diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java index bbba06b8c0e..7790a492375 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class MethodRef { public final long cls; public final long name; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java index f0d7b9d0905..01e292f96f4 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class StackTrace { public final long[] methods; public final byte[] types; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java index 337cbeef563..0f60086527d 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class AllocationSample extends Event { public final int classId; public final long allocationSize; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java index f8632a21bd3..9134fe190ec 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public final class CPULoad extends Event { public final float jvmUser; public final float jvmSystem; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java index c0cc52924a4..e85595af4ce 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class ContendedLock extends Event { public final long duration; public final int classId; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java index 323ffb327b6..5612904e404 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import java.lang.reflect.Field; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public abstract class Event implements Comparable { public final long time; public final int tid; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java index 23c9f7aa29c..a3b9c7dd17b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java @@ -5,8 +5,10 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +@ApiStatus.Internal public final class EventAggregator implements EventCollector { private static final int INITIAL_CAPACITY = 1024; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java index ac12de630f6..639faa88778 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public interface EventCollector { void collect(Event e); diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java index 9bbbea38c72..d4db5c5e585 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class ExecutionSample extends Event { public final int threadState; public final int samples; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java index 740fbc82245..68a8be94cf5 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public final class GCHeapSummary extends Event { public final int gcId; public final boolean afterGC; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java index 9fcf776ee6b..6423d3b7f67 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class LiveObject extends Event { public final int classId; public final long allocationSize; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java index eac63a518d0..04aff9c71fe 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java @@ -5,6 +5,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal public final class MallocEvent extends Event { public final long address; public final long size; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java index cde4919bd3e..6fc81957342 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java @@ -9,8 +9,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +@ApiStatus.Internal public final class MallocLeakAggregator implements EventCollector { private final EventCollector wrapped; private final Map addresses; diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java index dbec70770df..202619272eb 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java @@ -6,7 +6,9 @@ package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public final class ObjectCount extends Event { public final int gcId; public final int classId; diff --git a/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java index 087d44f61c0..8e3ed8fd5a2 100644 --- a/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java +++ b/sentry/src/main/java/io/sentry/profiling/JavaContinuousProfilerProvider.java @@ -3,6 +3,7 @@ import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -11,6 +12,7 @@ *

This interface allows for pluggable continuous profiler implementations that can be discovered * at runtime using the ServiceLoader mechanism. */ +@ApiStatus.Internal public interface JavaContinuousProfilerProvider { /** diff --git a/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java index 34ac31c66f0..e1fcdfa8793 100644 --- a/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java +++ b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java @@ -1,6 +1,7 @@ package io.sentry.profiling; import io.sentry.IProfileConverter; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** @@ -9,6 +10,7 @@ *

This interface allows for pluggable profile converter implementations that can be discovered * at runtime using the ServiceLoader mechanism. */ +@ApiStatus.Internal public interface JavaProfileConverterProvider { /** diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index ef5a9373c42..1bececbd765 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -9,9 +9,12 @@ import io.sentry.SentryLevel; import java.util.Iterator; import java.util.ServiceLoader; + +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class ProfilingServiceLoader { public static @NotNull IContinuousProfiler loadContinuousProfiler( From c404b77c13b9ab0ce40460191cca6f44493f9800 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 29 Sep 2025 13:37:23 +0000 Subject: [PATCH 22/31] Format code --- .../main/java/io/sentry/profiling/ProfilingServiceLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index 1bececbd765..b52cc8feaa0 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -9,7 +9,6 @@ import io.sentry.SentryLevel; import java.util.Iterator; import java.util.ServiceLoader; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; From 3d93ba0c9a0ab699854cfec8589baa21c805922f Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 29 Sep 2025 15:52:51 +0200 Subject: [PATCH 23/31] mark Sentry profile classes internal --- .../asyncprofiler/convert/Arguments.java | 132 --------------- .../asyncprofiler/convert/Classifier.java | 156 ----------------- .../vendor/asyncprofiler/convert/Frame.java | 68 -------- .../vendor/asyncprofiler/jfr/ClassRef.java | 17 -- .../vendor/asyncprofiler/jfr/Dictionary.java | 116 ------------- .../asyncprofiler/jfr/DictionaryInt.java | 125 -------------- .../vendor/asyncprofiler/jfr/Element.java | 18 -- .../vendor/asyncprofiler/jfr/JfrClass.java | 44 ----- .../vendor/asyncprofiler/jfr/JfrField.java | 24 --- .../vendor/asyncprofiler/jfr/MethodRef.java | 21 --- .../vendor/asyncprofiler/jfr/StackTrace.java | 21 --- .../jfr/event/AllocationSample.java | 47 ------ .../asyncprofiler/jfr/event/CPULoad.java | 23 --- .../jfr/event/ContendedLock.java | 44 ----- .../vendor/asyncprofiler/jfr/event/Event.java | 68 -------- .../jfr/event/EventAggregator.java | 157 ------------------ .../jfr/event/EventCollector.java | 27 --- .../jfr/event/ExecutionSample.java | 30 ---- .../jfr/event/GCHeapSummary.java | 30 ---- .../asyncprofiler/jfr/event/LiveObject.java | 47 ------ .../asyncprofiler/jfr/event/MallocEvent.java | 25 --- .../jfr/event/MallocLeakAggregator.java | 69 -------- .../asyncprofiler/jfr/event/ObjectCount.java | 25 --- .../src/main/java/io/sentry/SpanContext.java | 2 + .../protocol/profiling/SentryProfile.java | 3 + .../protocol/profiling/SentrySample.java | 3 + 26 files changed, 8 insertions(+), 1334 deletions(-) delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java delete mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java deleted file mode 100644 index f3b44fccc09..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.regex.Pattern; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class Arguments { - public @NotNull String title = "Flame Graph"; - public @Nullable String highlight; - public @Nullable String output; - public @Nullable String state; - public @Nullable Pattern include; - public @Nullable Pattern exclude; - public double minwidth; - public double grain; - public int skip; - public boolean help; - public boolean reverse; - public boolean inverted; - public boolean cpu; - public boolean wall; - public boolean alloc; - public boolean nativemem; - public boolean leak; - public boolean live; - public boolean lock; - public boolean threads; - public boolean classify; - public boolean total; - public boolean lines; - public boolean bci; - public boolean simple; - public boolean norm; - public boolean dot; - public long from; - public long to; - public final List files = new ArrayList<>(); - - public Arguments(String... args) { - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - String fieldName; - if (arg.startsWith("--")) { - fieldName = arg.substring(2); - } else if (arg.startsWith("-") && arg.length() == 2) { - fieldName = alias(arg.charAt(1)); - } else { - files.add(arg); - continue; - } - - try { - Field f = Arguments.class.getDeclaredField(fieldName); - if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) { - throw new IllegalArgumentException(arg); - } - - Class type = f.getType(); - if (type == String.class) { - f.set(this, args[++i]); - } else if (type == boolean.class) { - f.setBoolean(this, true); - } else if (type == int.class) { - f.setInt(this, Integer.parseInt(args[++i])); - } else if (type == double.class) { - f.setDouble(this, Double.parseDouble(args[++i])); - } else if (type == long.class) { - f.setLong(this, parseTimestamp(args[++i])); - } else if (type == Pattern.class) { - f.set(this, Pattern.compile(args[++i])); - } - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new IllegalArgumentException(arg); - } - } - } - - private static String alias(char c) { - switch (c) { - case 'h': - return "help"; - case 'o': - return "output"; - case 'r': - return "reverse"; - case 'i': - return "inverted"; - case 'I': - return "include"; - case 'X': - return "exclude"; - case 't': - return "threads"; - case 's': - return "state"; - default: - return String.valueOf(c); - } - } - - // Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S - private static long parseTimestamp(String time) { - if (time.indexOf(':') < 0) { - return Long.parseLong(time); - } - - GregorianCalendar cal = new GregorianCalendar(); - StringTokenizer st = new StringTokenizer(time, "-:.T"); - - if (time.indexOf('T') > 0) { - cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken())); - cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1); - cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken())); - } - cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); - - return cal.getTimeInMillis(); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java deleted file mode 100644 index ec955870930..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; - -import static io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Frame.*; - -import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -abstract class Classifier { - - enum Category { - GC("[gc]", TYPE_CPP), - JIT("[jit]", TYPE_CPP), - VM("[vm]", TYPE_CPP), - VTABLE_STUBS("[vtable_stubs]", TYPE_NATIVE), - NATIVE("[native]", TYPE_NATIVE), - INTERPRETER("[Interpreter]", TYPE_NATIVE), - C1_COMP("[c1_comp]", TYPE_C1_COMPILED), - C2_COMP("[c2_comp]", TYPE_INLINED), - ADAPTER("[c2i_adapter]", TYPE_INLINED), - CLASS_INIT("[class_init]", TYPE_CPP), - CLASS_LOAD("[class_load]", TYPE_CPP), - CLASS_RESOLVE("[class_resolve]", TYPE_CPP), - CLASS_VERIFY("[class_verify]", TYPE_CPP), - LAMBDA_INIT("[lambda_init]", TYPE_CPP); - - final String title; - final byte type; - - Category(String title, byte type) { - this.title = title; - this.type = type; - } - } - - public @Nullable Category getCategory(@NotNull StackTrace stackTrace) { - long[] methods = stackTrace.methods; - byte[] types = stackTrace.types; - - Category category; - if ((category = detectGcJit(methods, types)) == null - && (category = detectClassLoading(methods, types)) == null) { - category = detectOther(methods, types); - } - return category; - } - - private @Nullable Category detectGcJit(long[] methods, byte[] types) { - boolean vmThread = false; - for (int i = types.length; --i >= 0; ) { - if (types[i] == TYPE_CPP) { - switch (getMethodName(methods[i], types[i])) { - case "CompileBroker::compiler_thread_loop": - return Category.JIT; - case "GCTaskThread::run": - case "WorkerThread::run": - return Category.GC; - case "java_start": - case "thread_native_entry": - vmThread = true; - break; - } - } else if (types[i] != TYPE_NATIVE) { - break; - } - } - return vmThread ? Category.VM : null; - } - - private @Nullable Category detectClassLoading(long[] methods, byte[] types) { - for (int i = 0; i < methods.length; i++) { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.equals("Verifier::verify")) { - return Category.CLASS_VERIFY; - } else if (methodName.startsWith("InstanceKlass::initialize")) { - return Category.CLASS_INIT; - } else if (methodName.startsWith("LinkResolver::") - || methodName.startsWith("InterpreterRuntime::resolve") - || methodName.startsWith("SystemDictionary::resolve")) { - return Category.CLASS_RESOLVE; - } else if (methodName.endsWith("ClassLoader.loadClass")) { - return Category.CLASS_LOAD; - } else if (methodName.endsWith("LambdaMetafactory.metafactory") - || methodName.endsWith("LambdaMetafactory.altMetafactory")) { - return Category.LAMBDA_INIT; - } else if (methodName.endsWith("table stub")) { - return Category.VTABLE_STUBS; - } else if (methodName.equals("Interpreter")) { - return Category.INTERPRETER; - } else if (methodName.startsWith("I2C/C2I")) { - return i + 1 < types.length && types[i + 1] == TYPE_INTERPRETED - ? Category.INTERPRETER - : Category.ADAPTER; - } - } - return null; - } - - private @NotNull Category detectOther(long[] methods, byte[] types) { - boolean inJava = true; - for (int i = 0; i < types.length; i++) { - switch (types[i]) { - case TYPE_INTERPRETED: - return inJava ? Category.INTERPRETER : Category.NATIVE; - case TYPE_JIT_COMPILED: - return inJava ? Category.C2_COMP : Category.NATIVE; - case TYPE_INLINED: - inJava = true; - break; - case TYPE_NATIVE: - { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.startsWith("JVM_") - || methodName.startsWith("Unsafe_") - || methodName.startsWith("MHN_") - || methodName.startsWith("jni_")) { - return Category.VM; - } - switch (methodName) { - case "call_stub": - case "deoptimization": - case "unknown_Java": - case "not_walkable_Java": - case "InlineCacheBuffer": - return Category.VM; - } - if (methodName.endsWith("_arraycopy") || methodName.contains("pthread_cond")) { - break; - } - inJava = false; - break; - } - case TYPE_CPP: - { - String methodName = getMethodName(methods[i], types[i]); - if (methodName.startsWith("Runtime1::")) { - return Category.C1_COMP; - } - break; - } - case TYPE_C1_COMPILED: - return inJava ? Category.C1_COMP : Category.NATIVE; - } - } - return Category.NATIVE; - } - - protected abstract @NotNull String getMethodName(long method, byte type); -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java deleted file mode 100644 index bb9768442c4..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; - -import java.util.HashMap; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class Frame extends HashMap { - private static final long serialVersionUID = 1L; - public static final byte TYPE_INTERPRETED = 0; - public static final byte TYPE_JIT_COMPILED = 1; - public static final byte TYPE_INLINED = 2; - public static final byte TYPE_NATIVE = 3; - public static final byte TYPE_CPP = 4; - public static final byte TYPE_KERNEL = 5; - public static final byte TYPE_C1_COMPILED = 6; - - private static final int TYPE_SHIFT = 28; - - final int key; - long total; - long self; - long inlined, c1, interpreted; - - private Frame(int key) { - this.key = key; - } - - Frame(int titleIndex, byte type) { - this(titleIndex | type << TYPE_SHIFT); - } - - Frame getChild(int titleIndex, byte type) { - return super.computeIfAbsent(titleIndex | type << TYPE_SHIFT, Frame::new); - } - - int getTitleIndex() { - return key & ((1 << TYPE_SHIFT) - 1); - } - - byte getType() { - if (inlined * 3 >= total) { - return TYPE_INLINED; - } else if (c1 * 2 >= total) { - return TYPE_C1_COMPILED; - } else if (interpreted * 2 >= total) { - return TYPE_INTERPRETED; - } else { - return (byte) (key >>> TYPE_SHIFT); - } - } - - int depth(long cutoff) { - int depth = 0; - if (size() > 0) { - for (Frame child : values()) { - if (child.total >= cutoff) { - depth = Math.max(depth, child.depth(cutoff)); - } - } - } - return depth + 1; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java deleted file mode 100644 index 1727dc2156c..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class ClassRef { - public final long name; - - public ClassRef(long name) { - this.name = name; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java deleted file mode 100644 index 5f3191a37e6..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import java.util.Arrays; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -/** Fast and compact long->Object map. */ -public final class Dictionary { - private static final int INITIAL_CAPACITY = 16; - - private long[] keys; - private Object[] values; - private int size; - - public Dictionary() { - this(INITIAL_CAPACITY); - } - - public Dictionary(int initialCapacity) { - this.keys = new long[initialCapacity]; - this.values = new Object[initialCapacity]; - } - - public void clear() { - Arrays.fill(keys, 0); - Arrays.fill(values, null); - size = 0; - } - - public int size() { - return size; - } - - public void put(long key, T value) { - if (key == 0) { - throw new IllegalArgumentException("Zero key not allowed"); - } - - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != 0) { - if (keys[i] == key) { - values[i] = value; - return; - } - i = (i + 1) & mask; - } - keys[i] = key; - values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } - } - - @SuppressWarnings("unchecked") - public T get(long key) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key && keys[i] != 0) { - i = (i + 1) & mask; - } - return (T) values[i]; - } - - @SuppressWarnings("unchecked") - public void forEach(Visitor visitor) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - visitor.visit(keys[i], (T) values[i]); - } - } - } - - public int preallocate(int count) { - if (count * 2 > keys.length) { - resize(Integer.highestOneBit(count * 4 - 1)); - } - return count; - } - - private void resize(int newCapacity) { - long[] newKeys = new long[newCapacity]; - Object[] newValues = new Object[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == 0) { - newKeys[j] = keys[i]; - newValues[j] = values[i]; - break; - } - } - } - } - - keys = newKeys; - values = newValues; - } - - private static int hashCode(long key) { - key *= 0xc6a4a7935bd1e995L; - return (int) (key ^ (key >>> 32)); - } - - public interface Visitor { - void visit(long key, T value); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java deleted file mode 100644 index 83d3a2772e1..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import java.util.Arrays; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -/** Fast and compact long->int map. */ -public final class DictionaryInt { - private static final int INITIAL_CAPACITY = 16; - - private long[] keys; - private int[] values; - private int size; - - public DictionaryInt() { - this(INITIAL_CAPACITY); - } - - public DictionaryInt(int initialCapacity) { - this.keys = new long[initialCapacity]; - this.values = new int[initialCapacity]; - } - - public void clear() { - Arrays.fill(keys, 0); - Arrays.fill(values, 0); - size = 0; - } - - public void put(long key, int value) { - if (key == 0) { - throw new IllegalArgumentException("Zero key not allowed"); - } - - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != 0) { - if (keys[i] == key) { - values[i] = value; - return; - } - i = (i + 1) & mask; - } - keys[i] = key; - values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } - } - - public int get(long key) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key) { - if (keys[i] == 0) { - throw new IllegalArgumentException("No such key: " + key); - } - i = (i + 1) & mask; - } - return values[i]; - } - - public int get(long key, int notFound) { - int mask = keys.length - 1; - int i = hashCode(key) & mask; - while (keys[i] != key) { - if (keys[i] == 0) { - return notFound; - } - i = (i + 1) & mask; - } - return values[i]; - } - - public void forEach(Visitor visitor) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - visitor.visit(keys[i], values[i]); - } - } - } - - public int preallocate(int count) { - if (count * 2 > keys.length) { - resize(Integer.highestOneBit(count * 4 - 1)); - } - return count; - } - - private void resize(int newCapacity) { - long[] newKeys = new long[newCapacity]; - int[] newValues = new int[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != 0) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == 0) { - newKeys[j] = keys[i]; - newValues[j] = values[i]; - break; - } - } - } - } - - keys = newKeys; - values = newValues; - } - - private static int hashCode(long key) { - key *= 0xc6a4a7935bd1e995L; - return (int) (key ^ (key >>> 32)); - } - - public interface Visitor { - void visit(long key, int value); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java deleted file mode 100644 index 0c12292106a..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -abstract class Element { - - void addChild(Element e) {} - - static final class NoOpElement extends Element { - // Empty implementation for unhandled element types - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java deleted file mode 100644 index eb85be46eb3..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class JfrClass extends Element { - final int id; - final boolean simpleType; - final @Nullable String name; - final List fields; - - JfrClass(@NotNull Map attributes) { - this.id = Integer.parseInt(attributes.get("id")); - this.simpleType = "true".equals(attributes.get("simpleType")); - this.name = attributes.get("name"); - this.fields = new ArrayList<>(2); - } - - @Override - void addChild(Element e) { - if (e instanceof JfrField) { - fields.add((JfrField) e); - } - } - - public @Nullable JfrField field(@NotNull String name) { - for (JfrField field : fields) { - if (field.name != null && field.name.equals(name)) { - return field; - } - } - return null; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java deleted file mode 100644 index 635b87fd0f3..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class JfrField extends Element { - final @Nullable String name; - final int type; - final boolean constantPool; - - JfrField(@NotNull Map attributes) { - this.name = attributes.get("name"); - this.type = Integer.parseInt(attributes.get("class")); - this.constantPool = "true".equals(attributes.get("constantPool")); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java deleted file mode 100644 index 7790a492375..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class MethodRef { - public final long cls; - public final long name; - public final long sig; - - public MethodRef(long cls, long name, long sig) { - this.cls = cls; - this.name = name; - this.sig = sig; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java deleted file mode 100644 index 01e292f96f4..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class StackTrace { - public final long[] methods; - public final byte[] types; - public final int[] locations; - - public StackTrace(long[] methods, byte[] types, int[] locations) { - this.methods = methods; - this.types = types; - this.locations = locations; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java deleted file mode 100644 index 0f60086527d..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class AllocationSample extends Event { - public final int classId; - public final long allocationSize; - public final long tlabSize; - - public AllocationSample( - long time, int tid, int stackTraceId, int classId, long allocationSize, long tlabSize) { - super(time, tid, stackTraceId); - this.classId = classId; - this.allocationSize = allocationSize; - this.tlabSize = tlabSize; - } - - @Override - public int hashCode() { - return classId * 127 + stackTraceId + (tlabSize == 0 ? 17 : 0); - } - - @Override - public boolean sameGroup(Event o) { - if (o instanceof AllocationSample) { - AllocationSample a = (AllocationSample) o; - return classId == a.classId && (tlabSize == 0) == (a.tlabSize == 0); - } - return false; - } - - @Override - public long classId() { - return classId; - } - - @Override - public long value() { - return tlabSize != 0 ? tlabSize : allocationSize; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java deleted file mode 100644 index 9134fe190ec..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class CPULoad extends Event { - public final float jvmUser; - public final float jvmSystem; - public final float machineTotal; - - public CPULoad(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.jvmUser = jfr.getFloat(); - this.jvmSystem = jfr.getFloat(); - this.machineTotal = jfr.getFloat(); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java deleted file mode 100644 index e85595af4ce..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class ContendedLock extends Event { - public final long duration; - public final int classId; - - public ContendedLock(long time, int tid, int stackTraceId, long duration, int classId) { - super(time, tid, stackTraceId); - this.duration = duration; - this.classId = classId; - } - - @Override - public int hashCode() { - return classId * 127 + stackTraceId; - } - - @Override - public boolean sameGroup(Event o) { - if (o instanceof ContendedLock) { - ContendedLock c = (ContendedLock) o; - return classId == c.classId; - } - return false; - } - - @Override - public long classId() { - return classId; - } - - @Override - public long value() { - return duration; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java deleted file mode 100644 index 5612904e404..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import java.lang.reflect.Field; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public abstract class Event implements Comparable { - public final long time; - public final int tid; - public final int stackTraceId; - - protected Event(long time, int tid, int stackTraceId) { - this.time = time; - this.tid = tid; - this.stackTraceId = stackTraceId; - } - - @Override - public int compareTo(Event o) { - return Long.compare(time, o.time); - } - - @Override - public int hashCode() { - return stackTraceId; - } - - @Override - public String toString() { - StringBuilder sb = - new StringBuilder(getClass().getSimpleName()) - .append("{time=") - .append(time) - .append(",tid=") - .append(tid) - .append(",stackTraceId=") - .append(stackTraceId); - for (Field f : getClass().getDeclaredFields()) { - try { - sb.append(',').append(f.getName()).append('=').append(f.get(this)); - } catch (ReflectiveOperationException e) { - break; - } - } - return sb.append('}').toString(); - } - - public boolean sameGroup(Event o) { - return getClass() == o.getClass(); - } - - public long classId() { - return 0; - } - - public long samples() { - return 1; - } - - public long value() { - return 1; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java deleted file mode 100644 index a3b9c7dd17b..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public final class EventAggregator implements EventCollector { - private static final int INITIAL_CAPACITY = 1024; - - private final boolean threads; - private final double grain; - private @NotNull Event[] keys; - private @NotNull long[] samples; - private @NotNull long[] values; - private int size; - private double fraction; - - public EventAggregator(boolean threads, double grain) { - this.threads = threads; - this.grain = grain; - this.keys = new Event[INITIAL_CAPACITY]; - this.samples = new long[INITIAL_CAPACITY]; - this.values = new long[INITIAL_CAPACITY]; - - beforeChunk(); - } - - public int size() { - return size; - } - - @Override - public void collect(Event e) { - collect(e, e.samples(), e.value()); - } - - public void collect(Event e, long samples, long value) { - int mask = keys.length - 1; - int i = hashCode(e) & mask; - while (keys[i] != null) { - if (sameGroup(keys[i], e)) { - this.samples[i] += samples; - this.values[i] += value; - return; - } - i = (i + 1) & mask; - } - - this.keys[i] = e; - this.samples[i] = samples; - this.values[i] = value; - - if (++size * 2 > keys.length) { - resize(keys.length * 2); - } - } - - @Override - public void beforeChunk() { - if (keys == null || size > 0) { - keys = new Event[INITIAL_CAPACITY]; - samples = new long[INITIAL_CAPACITY]; - values = new long[INITIAL_CAPACITY]; - size = 0; - } - } - - @Override - public void afterChunk() { - if (grain > 0) { - coarsen(grain); - } - } - - @Override - public boolean finish() { - // Don't set to null as it would break nullability contract - keys = new Event[0]; - samples = new long[0]; - values = new long[0]; - return false; - } - - @Override - public void forEach(Visitor visitor) { - if (size > 0) { - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - visitor.visit(keys[i], samples[i], values[i]); - } - } - } - } - - public void coarsen(double grain) { - fraction = 0; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - long s0 = samples[i]; - long s1 = round(s0 / grain); - if (s1 == 0) { - keys[i] = null; - size--; - } - samples[i] = s1; - values[i] = (long) (values[i] * ((double) s1 / s0)); - } - } - } - - private long round(double d) { - long r = (long) d; - if ((fraction += d - r) >= 1.0) { - fraction -= 1.0; - r++; - } - return r; - } - - private int hashCode(Event e) { - return e.hashCode() + (threads ? e.tid * 31 : 0); - } - - private boolean sameGroup(Event e1, Event e2) { - return e1.stackTraceId == e2.stackTraceId && (!threads || e1.tid == e2.tid) && e1.sameGroup(e2); - } - - private void resize(int newCapacity) { - Event[] newKeys = new Event[newCapacity]; - long[] newSamples = new long[newCapacity]; - long[] newValues = new long[newCapacity]; - int mask = newKeys.length - 1; - - for (int i = 0; i < keys.length; i++) { - if (keys[i] != null) { - for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { - if (newKeys[j] == null) { - newKeys[j] = keys[i]; - newSamples[j] = samples[i]; - newValues[j] = values[i]; - break; - } - } - } - } - - keys = newKeys; - samples = newSamples; - values = newValues; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java deleted file mode 100644 index 639faa88778..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public interface EventCollector { - - void collect(Event e); - - void beforeChunk(); - - void afterChunk(); - - // Returns true if this collector has remaining data to process - boolean finish(); - - void forEach(Visitor visitor); - - interface Visitor { - void visit(Event event, long samples, long value); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java deleted file mode 100644 index d4db5c5e585..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class ExecutionSample extends Event { - public final int threadState; - public final int samples; - - public ExecutionSample(long time, int tid, int stackTraceId, int threadState, int samples) { - super(time, tid, stackTraceId); - this.threadState = threadState; - this.samples = samples; - } - - @Override - public long samples() { - return samples; - } - - @Override - public long value() { - return samples; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java deleted file mode 100644 index 68a8be94cf5..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class GCHeapSummary extends Event { - public final int gcId; - public final boolean afterGC; - public final long committed; - public final long reserved; - public final long used; - - public GCHeapSummary(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.gcId = jfr.getVarint(); - this.afterGC = jfr.getVarint() > 0; - jfr.getVarlong(); // long start - jfr.getVarlong(); // long committedEnd - this.committed = jfr.getVarlong(); - jfr.getVarlong(); // long reservedEnd - this.reserved = jfr.getVarlong(); - this.used = jfr.getVarlong(); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java deleted file mode 100644 index 6423d3b7f67..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class LiveObject extends Event { - public final int classId; - public final long allocationSize; - public final long allocationTime; - - public LiveObject( - long time, int tid, int stackTraceId, int classId, long allocationSize, long allocationTime) { - super(time, tid, stackTraceId); - this.classId = classId; - this.allocationSize = allocationSize; - this.allocationTime = allocationTime; - } - - @Override - public int hashCode() { - return classId * 127 + stackTraceId; - } - - @Override - public boolean sameGroup(Event o) { - if (o instanceof LiveObject) { - LiveObject a = (LiveObject) o; - return classId == a.classId; - } - return false; - } - - @Override - public long classId() { - return classId; - } - - @Override - public long value() { - return allocationSize; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java deleted file mode 100644 index 04aff9c71fe..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class MallocEvent extends Event { - public final long address; - public final long size; - - public MallocEvent(long time, int tid, int stackTraceId, long address, long size) { - super(time, tid, stackTraceId); - this.address = address; - this.size = size; - } - - @Override - public long value() { - return size; - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java deleted file mode 100644 index 6fc81957342..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public final class MallocLeakAggregator implements EventCollector { - private final EventCollector wrapped; - private final Map addresses; - private @NotNull List events; - - public MallocLeakAggregator(@NotNull EventCollector wrapped) { - this.wrapped = wrapped; - this.addresses = new HashMap<>(); - this.events = new ArrayList<>(); - } - - @Override - public void collect(Event e) { - events.add((MallocEvent) e); - } - - @Override - public void beforeChunk() { - events = new ArrayList<>(); - } - - @Override - public void afterChunk() { - events.sort(null); - - for (MallocEvent e : events) { - if (e.size > 0) { - addresses.put(e.address, e); - } else { - addresses.remove(e.address); - } - } - - events = new ArrayList<>(); - } - - @Override - public boolean finish() { - wrapped.beforeChunk(); - for (Event e : addresses.values()) { - wrapped.collect(e); - } - wrapped.afterChunk(); - - // Free memory before the final conversion - addresses.clear(); - return true; - } - - @Override - public void forEach(Visitor visitor) { - wrapped.forEach(visitor); - } -} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java deleted file mode 100644 index 202619272eb..00000000000 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright The async-profiler authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; - -import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public final class ObjectCount extends Event { - public final int gcId; - public final int classId; - public final long count; - public final long totalSize; - - public ObjectCount(JfrReader jfr) { - super(jfr.getVarlong(), 0, 0); - this.gcId = jfr.getVarint(); - this.classId = jfr.getVarint(); - this.count = jfr.getVarlong(); - this.totalSize = jfr.getVarlong(); - } -} diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 8bfc83e6458..af981a6d245 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -310,10 +310,12 @@ public int hashCode() { return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus()); } + @ApiStatus.Internal public @NotNull SentryId getProfilerId() { return profilerId; } + @ApiStatus.Internal public void setProfilerId(@NotNull SentryId profilerId) { this.profilerId = profilerId; } 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 ad64b880625..a333108990f 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java @@ -14,9 +14,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class SentryProfile implements JsonUnknown, JsonSerializable { private @NotNull List samples = new ArrayList<>(); diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java index 83e46023e08..e552b7832a0 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java @@ -12,9 +12,12 @@ import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; + +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class SentrySample implements JsonUnknown, JsonSerializable { private double timestamp; From 1321c030e03f856c2145c9aeaecb0db0f4f4b09e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 29 Sep 2025 13:56:12 +0000 Subject: [PATCH 24/31] Format code --- .../main/java/io/sentry/protocol/profiling/SentryProfile.java | 1 - .../src/main/java/io/sentry/protocol/profiling/SentrySample.java | 1 - 2 files changed, 2 deletions(-) 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 a333108990f..6a3c8d01f12 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryProfile.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java index e552b7832a0..dc47b1b1fac 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java @@ -12,7 +12,6 @@ import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; From 22987485e3069524eed3f2f822493acb03555aca Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 13:04:39 +0200 Subject: [PATCH 25/31] mark SentryThreadMetadata internal --- .../io/sentry/protocol/profiling/SentryThreadMetadata.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java index a4c540d3b66..484c02460a5 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java @@ -10,9 +10,12 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; + +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Internal public final class SentryThreadMetadata implements JsonUnknown, JsonSerializable { private @Nullable String name; From 9a11e02a3adb9f08c5b74a21b9cea6c08f800f41 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 13:07:49 +0200 Subject: [PATCH 26/31] add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8fb0ca1af8..79bf01e9cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Features +- Add support for continuous profiling of JVM applications on macOS and Linux ([#4556](https://github.com/getsentry/sentry-java/pull/4556)) + - Sentry continuous profiling on the JVM is using async-profiler under the hood. + ### Fixes - Start performance collection on AppStart continuous profiling ([#4752](https://github.com/getsentry/sentry-java/pull/4752)) From e5ffefb970a00122b8a357488dca11e352a5e60b Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 30 Sep 2025 11:08:31 +0000 Subject: [PATCH 27/31] Format code --- .../java/io/sentry/protocol/profiling/SentryThreadMetadata.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java index 484c02460a5..9fb6c256bb1 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; From b065ef1587aa2bafe4c0034c1e8d6109dc94def5 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 15:05:00 +0200 Subject: [PATCH 28/31] re-add asyncprofiler classes, cr changes --- .../profiling/JavaContinuousProfiler.java | 52 ++++-- .../asyncprofiler/convert/Arguments.java | 132 +++++++++++++++ .../asyncprofiler/convert/Classifier.java | 156 +++++++++++++++++ .../vendor/asyncprofiler/convert/Frame.java | 68 ++++++++ .../vendor/asyncprofiler/jfr/ClassRef.java | 17 ++ .../vendor/asyncprofiler/jfr/Dictionary.java | 116 +++++++++++++ .../asyncprofiler/jfr/DictionaryInt.java | 125 ++++++++++++++ .../vendor/asyncprofiler/jfr/Element.java | 18 ++ .../vendor/asyncprofiler/jfr/JfrClass.java | 44 +++++ .../vendor/asyncprofiler/jfr/JfrField.java | 24 +++ .../vendor/asyncprofiler/jfr/MethodRef.java | 21 +++ .../vendor/asyncprofiler/jfr/StackTrace.java | 21 +++ .../jfr/event/AllocationSample.java | 47 ++++++ .../asyncprofiler/jfr/event/CPULoad.java | 23 +++ .../jfr/event/ContendedLock.java | 44 +++++ .../vendor/asyncprofiler/jfr/event/Event.java | 68 ++++++++ .../jfr/event/EventAggregator.java | 157 ++++++++++++++++++ .../jfr/event/EventCollector.java | 27 +++ .../jfr/event/ExecutionSample.java | 30 ++++ .../jfr/event/GCHeapSummary.java | 30 ++++ .../asyncprofiler/jfr/event/LiveObject.java | 47 ++++++ .../asyncprofiler/jfr/event/MallocEvent.java | 25 +++ .../jfr/event/MallocLeakAggregator.java | 69 ++++++++ .../asyncprofiler/jfr/event/ObjectCount.java | 25 +++ sentry/src/main/java/io/sentry/Sentry.java | 2 +- .../java/io/sentry/SentryEnvelopeItem.java | 5 +- .../exception/SentryEnvelopeException.java | 4 + .../profiling/ProfilingServiceLoader.java | 1 + .../protocol/profiling/SentrySample.java | 18 +- .../profiling/SentryThreadMetadata.java | 17 +- 30 files changed, 1413 insertions(+), 20 deletions(-) create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java create mode 100644 sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java 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 6b45d568394..2c6d7cc754c 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 @@ -25,6 +25,7 @@ import io.sentry.util.SentryRandom; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.Future; @@ -40,6 +41,10 @@ public final class JavaContinuousProfiler implements IContinuousProfiler, RateLimiter.IRateLimitObserver { private static final long MAX_CHUNK_DURATION_MILLIS = 10000; + private final List invalidFilenameChars = + Arrays.asList( + ".", ",", ":", ";", "|", "\\", "=", "%", "&", "[", "]", "(", ")", "<", ">", "{", "}", "!", + "*", "?", "~", "\"", "'", "$", "`", "^"); private final @NotNull ILogger logger; private final @Nullable String profilingTracesDirPath; @@ -54,7 +59,7 @@ public final class JavaContinuousProfiler private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); - private @NotNull String filename = ""; + private volatile @NotNull String filename = ""; private @NotNull AsyncProfiler profiler; private volatile boolean shouldSample = true; @@ -90,11 +95,36 @@ private boolean init() { } isInitialized = true; + if (isInvalidDirectory()) { + return false; + } + + if (profilingTracesHz <= 0) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + profilingTracesHz); + return false; + } + return true; + } + + private boolean isInvalidDirectory() { if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, "Disabling profiling because no profiling traces dir path is defined in options."); - return false; + return true; + } + + for (String invalidChar : invalidFilenameChars) { + if (profilingTracesDirPath.contains(invalidChar)) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because traces directory path contains invalid character: %s", + filename); + return true; + } } File profileDir = new File(profilingTracesDirPath); @@ -106,17 +136,9 @@ private boolean init() { profilingTracesDirPath, profileDir.canWrite(), profileDir.exists()); - return false; - } - - if (profilingTracesHz <= 0) { - logger.log( - SentryLevel.WARNING, - "Disabling profiling because trace rate is set to %d", - profilingTracesHz); - return false; + return true; } - return true; + return false; } @Override @@ -354,6 +376,12 @@ public void close(final boolean isTerminating) { return profilerId; } + // This is currently not used in the JVM profiler, thus we return an empty id for now + @Override + public @NotNull SentryId getChunkId() { + return SentryId.EMPTY_ID; + } + @SuppressWarnings("FutureReturnValueIgnored") private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { try { diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java new file mode 100644 index 00000000000..f3b44fccc09 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments.java @@ -0,0 +1,132 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.regex.Pattern; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class Arguments { + public @NotNull String title = "Flame Graph"; + public @Nullable String highlight; + public @Nullable String output; + public @Nullable String state; + public @Nullable Pattern include; + public @Nullable Pattern exclude; + public double minwidth; + public double grain; + public int skip; + public boolean help; + public boolean reverse; + public boolean inverted; + public boolean cpu; + public boolean wall; + public boolean alloc; + public boolean nativemem; + public boolean leak; + public boolean live; + public boolean lock; + public boolean threads; + public boolean classify; + public boolean total; + public boolean lines; + public boolean bci; + public boolean simple; + public boolean norm; + public boolean dot; + public long from; + public long to; + public final List files = new ArrayList<>(); + + public Arguments(String... args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + String fieldName; + if (arg.startsWith("--")) { + fieldName = arg.substring(2); + } else if (arg.startsWith("-") && arg.length() == 2) { + fieldName = alias(arg.charAt(1)); + } else { + files.add(arg); + continue; + } + + try { + Field f = Arguments.class.getDeclaredField(fieldName); + if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) { + throw new IllegalArgumentException(arg); + } + + Class type = f.getType(); + if (type == String.class) { + f.set(this, args[++i]); + } else if (type == boolean.class) { + f.setBoolean(this, true); + } else if (type == int.class) { + f.setInt(this, Integer.parseInt(args[++i])); + } else if (type == double.class) { + f.setDouble(this, Double.parseDouble(args[++i])); + } else if (type == long.class) { + f.setLong(this, parseTimestamp(args[++i])); + } else if (type == Pattern.class) { + f.set(this, Pattern.compile(args[++i])); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalArgumentException(arg); + } + } + } + + private static String alias(char c) { + switch (c) { + case 'h': + return "help"; + case 'o': + return "output"; + case 'r': + return "reverse"; + case 'i': + return "inverted"; + case 'I': + return "include"; + case 'X': + return "exclude"; + case 't': + return "threads"; + case 's': + return "state"; + default: + return String.valueOf(c); + } + } + + // Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S + private static long parseTimestamp(String time) { + if (time.indexOf(':') < 0) { + return Long.parseLong(time); + } + + GregorianCalendar cal = new GregorianCalendar(); + StringTokenizer st = new StringTokenizer(time, "-:.T"); + + if (time.indexOf('T') > 0) { + cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken())); + cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1); + cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken())); + } + cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0); + + return cal.getTimeInMillis(); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java new file mode 100644 index 00000000000..ec955870930 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Classifier.java @@ -0,0 +1,156 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; + +import static io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Frame.*; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.StackTrace; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +abstract class Classifier { + + enum Category { + GC("[gc]", TYPE_CPP), + JIT("[jit]", TYPE_CPP), + VM("[vm]", TYPE_CPP), + VTABLE_STUBS("[vtable_stubs]", TYPE_NATIVE), + NATIVE("[native]", TYPE_NATIVE), + INTERPRETER("[Interpreter]", TYPE_NATIVE), + C1_COMP("[c1_comp]", TYPE_C1_COMPILED), + C2_COMP("[c2_comp]", TYPE_INLINED), + ADAPTER("[c2i_adapter]", TYPE_INLINED), + CLASS_INIT("[class_init]", TYPE_CPP), + CLASS_LOAD("[class_load]", TYPE_CPP), + CLASS_RESOLVE("[class_resolve]", TYPE_CPP), + CLASS_VERIFY("[class_verify]", TYPE_CPP), + LAMBDA_INIT("[lambda_init]", TYPE_CPP); + + final String title; + final byte type; + + Category(String title, byte type) { + this.title = title; + this.type = type; + } + } + + public @Nullable Category getCategory(@NotNull StackTrace stackTrace) { + long[] methods = stackTrace.methods; + byte[] types = stackTrace.types; + + Category category; + if ((category = detectGcJit(methods, types)) == null + && (category = detectClassLoading(methods, types)) == null) { + category = detectOther(methods, types); + } + return category; + } + + private @Nullable Category detectGcJit(long[] methods, byte[] types) { + boolean vmThread = false; + for (int i = types.length; --i >= 0; ) { + if (types[i] == TYPE_CPP) { + switch (getMethodName(methods[i], types[i])) { + case "CompileBroker::compiler_thread_loop": + return Category.JIT; + case "GCTaskThread::run": + case "WorkerThread::run": + return Category.GC; + case "java_start": + case "thread_native_entry": + vmThread = true; + break; + } + } else if (types[i] != TYPE_NATIVE) { + break; + } + } + return vmThread ? Category.VM : null; + } + + private @Nullable Category detectClassLoading(long[] methods, byte[] types) { + for (int i = 0; i < methods.length; i++) { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.equals("Verifier::verify")) { + return Category.CLASS_VERIFY; + } else if (methodName.startsWith("InstanceKlass::initialize")) { + return Category.CLASS_INIT; + } else if (methodName.startsWith("LinkResolver::") + || methodName.startsWith("InterpreterRuntime::resolve") + || methodName.startsWith("SystemDictionary::resolve")) { + return Category.CLASS_RESOLVE; + } else if (methodName.endsWith("ClassLoader.loadClass")) { + return Category.CLASS_LOAD; + } else if (methodName.endsWith("LambdaMetafactory.metafactory") + || methodName.endsWith("LambdaMetafactory.altMetafactory")) { + return Category.LAMBDA_INIT; + } else if (methodName.endsWith("table stub")) { + return Category.VTABLE_STUBS; + } else if (methodName.equals("Interpreter")) { + return Category.INTERPRETER; + } else if (methodName.startsWith("I2C/C2I")) { + return i + 1 < types.length && types[i + 1] == TYPE_INTERPRETED + ? Category.INTERPRETER + : Category.ADAPTER; + } + } + return null; + } + + private @NotNull Category detectOther(long[] methods, byte[] types) { + boolean inJava = true; + for (int i = 0; i < types.length; i++) { + switch (types[i]) { + case TYPE_INTERPRETED: + return inJava ? Category.INTERPRETER : Category.NATIVE; + case TYPE_JIT_COMPILED: + return inJava ? Category.C2_COMP : Category.NATIVE; + case TYPE_INLINED: + inJava = true; + break; + case TYPE_NATIVE: + { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("JVM_") + || methodName.startsWith("Unsafe_") + || methodName.startsWith("MHN_") + || methodName.startsWith("jni_")) { + return Category.VM; + } + switch (methodName) { + case "call_stub": + case "deoptimization": + case "unknown_Java": + case "not_walkable_Java": + case "InlineCacheBuffer": + return Category.VM; + } + if (methodName.endsWith("_arraycopy") || methodName.contains("pthread_cond")) { + break; + } + inJava = false; + break; + } + case TYPE_CPP: + { + String methodName = getMethodName(methods[i], types[i]); + if (methodName.startsWith("Runtime1::")) { + return Category.C1_COMP; + } + break; + } + case TYPE_C1_COMPILED: + return inJava ? Category.C1_COMP : Category.NATIVE; + } + } + return Category.NATIVE; + } + + protected abstract @NotNull String getMethodName(long method, byte type); +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java new file mode 100644 index 00000000000..bb9768442c4 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/convert/Frame.java @@ -0,0 +1,68 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.convert; + +import java.util.HashMap; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class Frame extends HashMap { + private static final long serialVersionUID = 1L; + public static final byte TYPE_INTERPRETED = 0; + public static final byte TYPE_JIT_COMPILED = 1; + public static final byte TYPE_INLINED = 2; + public static final byte TYPE_NATIVE = 3; + public static final byte TYPE_CPP = 4; + public static final byte TYPE_KERNEL = 5; + public static final byte TYPE_C1_COMPILED = 6; + + private static final int TYPE_SHIFT = 28; + + final int key; + long total; + long self; + long inlined, c1, interpreted; + + private Frame(int key) { + this.key = key; + } + + Frame(int titleIndex, byte type) { + this(titleIndex | type << TYPE_SHIFT); + } + + Frame getChild(int titleIndex, byte type) { + return super.computeIfAbsent(titleIndex | type << TYPE_SHIFT, Frame::new); + } + + int getTitleIndex() { + return key & ((1 << TYPE_SHIFT) - 1); + } + + byte getType() { + if (inlined * 3 >= total) { + return TYPE_INLINED; + } else if (c1 * 2 >= total) { + return TYPE_C1_COMPILED; + } else if (interpreted * 2 >= total) { + return TYPE_INTERPRETED; + } else { + return (byte) (key >>> TYPE_SHIFT); + } + } + + int depth(long cutoff) { + int depth = 0; + if (size() > 0) { + for (Frame child : values()) { + if (child.total >= cutoff) { + depth = Math.max(depth, child.depth(cutoff)); + } + } + } + return depth + 1; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java new file mode 100644 index 00000000000..1727dc2156c --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/ClassRef.java @@ -0,0 +1,17 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ClassRef { + public final long name; + + public ClassRef(long name) { + this.name = name; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java new file mode 100644 index 00000000000..5f3191a37e6 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Dictionary.java @@ -0,0 +1,116 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import java.util.Arrays; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +/** Fast and compact long->Object map. */ +public final class Dictionary { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private Object[] values; + private int size; + + public Dictionary() { + this(INITIAL_CAPACITY); + } + + public Dictionary(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new Object[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, null); + size = 0; + } + + public int size() { + return size; + } + + public void put(long key, T value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); + } + + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; + } + keys[i] = key; + values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + @SuppressWarnings("unchecked") + public T get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key && keys[i] != 0) { + i = (i + 1) & mask; + } + return (T) values[i]; + } + + @SuppressWarnings("unchecked") + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], (T) values[i]); + } + } + } + + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); + } + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + Object[] newValues = new Object[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + values = newValues; + } + + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } + + public interface Visitor { + void visit(long key, T value); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java new file mode 100644 index 00000000000..83d3a2772e1 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/DictionaryInt.java @@ -0,0 +1,125 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import java.util.Arrays; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +/** Fast and compact long->int map. */ +public final class DictionaryInt { + private static final int INITIAL_CAPACITY = 16; + + private long[] keys; + private int[] values; + private int size; + + public DictionaryInt() { + this(INITIAL_CAPACITY); + } + + public DictionaryInt(int initialCapacity) { + this.keys = new long[initialCapacity]; + this.values = new int[initialCapacity]; + } + + public void clear() { + Arrays.fill(keys, 0); + Arrays.fill(values, 0); + size = 0; + } + + public void put(long key, int value) { + if (key == 0) { + throw new IllegalArgumentException("Zero key not allowed"); + } + + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != 0) { + if (keys[i] == key) { + values[i] = value; + return; + } + i = (i + 1) & mask; + } + keys[i] = key; + values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + public int get(long key) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + throw new IllegalArgumentException("No such key: " + key); + } + i = (i + 1) & mask; + } + return values[i]; + } + + public int get(long key, int notFound) { + int mask = keys.length - 1; + int i = hashCode(key) & mask; + while (keys[i] != key) { + if (keys[i] == 0) { + return notFound; + } + i = (i + 1) & mask; + } + return values[i]; + } + + public void forEach(Visitor visitor) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + visitor.visit(keys[i], values[i]); + } + } + } + + public int preallocate(int count) { + if (count * 2 > keys.length) { + resize(Integer.highestOneBit(count * 4 - 1)); + } + return count; + } + + private void resize(int newCapacity) { + long[] newKeys = new long[newCapacity]; + int[] newValues = new int[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != 0) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == 0) { + newKeys[j] = keys[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + values = newValues; + } + + private static int hashCode(long key) { + key *= 0xc6a4a7935bd1e995L; + return (int) (key ^ (key >>> 32)); + } + + public interface Visitor { + void visit(long key, int value); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java new file mode 100644 index 00000000000..0c12292106a --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/Element.java @@ -0,0 +1,18 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +abstract class Element { + + void addChild(Element e) {} + + static final class NoOpElement extends Element { + // Empty implementation for unhandled element types + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java new file mode 100644 index 00000000000..eb85be46eb3 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrClass.java @@ -0,0 +1,44 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class JfrClass extends Element { + final int id; + final boolean simpleType; + final @Nullable String name; + final List fields; + + JfrClass(@NotNull Map attributes) { + this.id = Integer.parseInt(attributes.get("id")); + this.simpleType = "true".equals(attributes.get("simpleType")); + this.name = attributes.get("name"); + this.fields = new ArrayList<>(2); + } + + @Override + void addChild(Element e) { + if (e instanceof JfrField) { + fields.add((JfrField) e); + } + } + + public @Nullable JfrField field(@NotNull String name) { + for (JfrField field : fields) { + if (field.name != null && field.name.equals(name)) { + return field; + } + } + return null; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java new file mode 100644 index 00000000000..635b87fd0f3 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrField.java @@ -0,0 +1,24 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class JfrField extends Element { + final @Nullable String name; + final int type; + final boolean constantPool; + + JfrField(@NotNull Map attributes) { + this.name = attributes.get("name"); + this.type = Integer.parseInt(attributes.get("class")); + this.constantPool = "true".equals(attributes.get("constantPool")); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java new file mode 100644 index 00000000000..7790a492375 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/MethodRef.java @@ -0,0 +1,21 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class MethodRef { + public final long cls; + public final long name; + public final long sig; + + public MethodRef(long cls, long name, long sig) { + this.cls = cls; + this.name = name; + this.sig = sig; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java new file mode 100644 index 00000000000..01e292f96f4 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/StackTrace.java @@ -0,0 +1,21 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class StackTrace { + public final long[] methods; + public final byte[] types; + public final int[] locations; + + public StackTrace(long[] methods, byte[] types, int[] locations) { + this.methods = methods; + this.types = types; + this.locations = locations; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java new file mode 100644 index 00000000000..0f60086527d --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/AllocationSample.java @@ -0,0 +1,47 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class AllocationSample extends Event { + public final int classId; + public final long allocationSize; + public final long tlabSize; + + public AllocationSample( + long time, int tid, int stackTraceId, int classId, long allocationSize, long tlabSize) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.tlabSize = tlabSize; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId + (tlabSize == 0 ? 17 : 0); + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof AllocationSample) { + AllocationSample a = (AllocationSample) o; + return classId == a.classId && (tlabSize == 0) == (a.tlabSize == 0); + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return tlabSize != 0 ? tlabSize : allocationSize; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java new file mode 100644 index 00000000000..9134fe190ec --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/CPULoad.java @@ -0,0 +1,23 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class CPULoad extends Event { + public final float jvmUser; + public final float jvmSystem; + public final float machineTotal; + + public CPULoad(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.jvmUser = jfr.getFloat(); + this.jvmSystem = jfr.getFloat(); + this.machineTotal = jfr.getFloat(); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java new file mode 100644 index 00000000000..e85595af4ce --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ContendedLock.java @@ -0,0 +1,44 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ContendedLock extends Event { + public final long duration; + public final int classId; + + public ContendedLock(long time, int tid, int stackTraceId, long duration, int classId) { + super(time, tid, stackTraceId); + this.duration = duration; + this.classId = classId; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof ContendedLock) { + ContendedLock c = (ContendedLock) o; + return classId == c.classId; + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return duration; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java new file mode 100644 index 00000000000..5612904e404 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/Event.java @@ -0,0 +1,68 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import java.lang.reflect.Field; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public abstract class Event implements Comparable { + public final long time; + public final int tid; + public final int stackTraceId; + + protected Event(long time, int tid, int stackTraceId) { + this.time = time; + this.tid = tid; + this.stackTraceId = stackTraceId; + } + + @Override + public int compareTo(Event o) { + return Long.compare(time, o.time); + } + + @Override + public int hashCode() { + return stackTraceId; + } + + @Override + public String toString() { + StringBuilder sb = + new StringBuilder(getClass().getSimpleName()) + .append("{time=") + .append(time) + .append(",tid=") + .append(tid) + .append(",stackTraceId=") + .append(stackTraceId); + for (Field f : getClass().getDeclaredFields()) { + try { + sb.append(',').append(f.getName()).append('=').append(f.get(this)); + } catch (ReflectiveOperationException e) { + break; + } + } + return sb.append('}').toString(); + } + + public boolean sameGroup(Event o) { + return getClass() == o.getClass(); + } + + public long classId() { + return 0; + } + + public long samples() { + return 1; + } + + public long value() { + return 1; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java new file mode 100644 index 00000000000..a3b9c7dd17b --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventAggregator.java @@ -0,0 +1,157 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class EventAggregator implements EventCollector { + private static final int INITIAL_CAPACITY = 1024; + + private final boolean threads; + private final double grain; + private @NotNull Event[] keys; + private @NotNull long[] samples; + private @NotNull long[] values; + private int size; + private double fraction; + + public EventAggregator(boolean threads, double grain) { + this.threads = threads; + this.grain = grain; + this.keys = new Event[INITIAL_CAPACITY]; + this.samples = new long[INITIAL_CAPACITY]; + this.values = new long[INITIAL_CAPACITY]; + + beforeChunk(); + } + + public int size() { + return size; + } + + @Override + public void collect(Event e) { + collect(e, e.samples(), e.value()); + } + + public void collect(Event e, long samples, long value) { + int mask = keys.length - 1; + int i = hashCode(e) & mask; + while (keys[i] != null) { + if (sameGroup(keys[i], e)) { + this.samples[i] += samples; + this.values[i] += value; + return; + } + i = (i + 1) & mask; + } + + this.keys[i] = e; + this.samples[i] = samples; + this.values[i] = value; + + if (++size * 2 > keys.length) { + resize(keys.length * 2); + } + } + + @Override + public void beforeChunk() { + if (keys == null || size > 0) { + keys = new Event[INITIAL_CAPACITY]; + samples = new long[INITIAL_CAPACITY]; + values = new long[INITIAL_CAPACITY]; + size = 0; + } + } + + @Override + public void afterChunk() { + if (grain > 0) { + coarsen(grain); + } + } + + @Override + public boolean finish() { + // Don't set to null as it would break nullability contract + keys = new Event[0]; + samples = new long[0]; + values = new long[0]; + return false; + } + + @Override + public void forEach(Visitor visitor) { + if (size > 0) { + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + visitor.visit(keys[i], samples[i], values[i]); + } + } + } + } + + public void coarsen(double grain) { + fraction = 0; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + long s0 = samples[i]; + long s1 = round(s0 / grain); + if (s1 == 0) { + keys[i] = null; + size--; + } + samples[i] = s1; + values[i] = (long) (values[i] * ((double) s1 / s0)); + } + } + } + + private long round(double d) { + long r = (long) d; + if ((fraction += d - r) >= 1.0) { + fraction -= 1.0; + r++; + } + return r; + } + + private int hashCode(Event e) { + return e.hashCode() + (threads ? e.tid * 31 : 0); + } + + private boolean sameGroup(Event e1, Event e2) { + return e1.stackTraceId == e2.stackTraceId && (!threads || e1.tid == e2.tid) && e1.sameGroup(e2); + } + + private void resize(int newCapacity) { + Event[] newKeys = new Event[newCapacity]; + long[] newSamples = new long[newCapacity]; + long[] newValues = new long[newCapacity]; + int mask = newKeys.length - 1; + + for (int i = 0; i < keys.length; i++) { + if (keys[i] != null) { + for (int j = hashCode(keys[i]) & mask; ; j = (j + 1) & mask) { + if (newKeys[j] == null) { + newKeys[j] = keys[i]; + newSamples[j] = samples[i]; + newValues[j] = values[i]; + break; + } + } + } + } + + keys = newKeys; + samples = newSamples; + values = newValues; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java new file mode 100644 index 00000000000..639faa88778 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/EventCollector.java @@ -0,0 +1,27 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface EventCollector { + + void collect(Event e); + + void beforeChunk(); + + void afterChunk(); + + // Returns true if this collector has remaining data to process + boolean finish(); + + void forEach(Visitor visitor); + + interface Visitor { + void visit(Event event, long samples, long value); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java new file mode 100644 index 00000000000..d4db5c5e585 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ExecutionSample.java @@ -0,0 +1,30 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ExecutionSample extends Event { + public final int threadState; + public final int samples; + + public ExecutionSample(long time, int tid, int stackTraceId, int threadState, int samples) { + super(time, tid, stackTraceId); + this.threadState = threadState; + this.samples = samples; + } + + @Override + public long samples() { + return samples; + } + + @Override + public long value() { + return samples; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java new file mode 100644 index 00000000000..68a8be94cf5 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/GCHeapSummary.java @@ -0,0 +1,30 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class GCHeapSummary extends Event { + public final int gcId; + public final boolean afterGC; + public final long committed; + public final long reserved; + public final long used; + + public GCHeapSummary(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.afterGC = jfr.getVarint() > 0; + jfr.getVarlong(); // long start + jfr.getVarlong(); // long committedEnd + this.committed = jfr.getVarlong(); + jfr.getVarlong(); // long reservedEnd + this.reserved = jfr.getVarlong(); + this.used = jfr.getVarlong(); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java new file mode 100644 index 00000000000..6423d3b7f67 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/LiveObject.java @@ -0,0 +1,47 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class LiveObject extends Event { + public final int classId; + public final long allocationSize; + public final long allocationTime; + + public LiveObject( + long time, int tid, int stackTraceId, int classId, long allocationSize, long allocationTime) { + super(time, tid, stackTraceId); + this.classId = classId; + this.allocationSize = allocationSize; + this.allocationTime = allocationTime; + } + + @Override + public int hashCode() { + return classId * 127 + stackTraceId; + } + + @Override + public boolean sameGroup(Event o) { + if (o instanceof LiveObject) { + LiveObject a = (LiveObject) o; + return classId == a.classId; + } + return false; + } + + @Override + public long classId() { + return classId; + } + + @Override + public long value() { + return allocationSize; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java new file mode 100644 index 00000000000..04aff9c71fe --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocEvent.java @@ -0,0 +1,25 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class MallocEvent extends Event { + public final long address; + public final long size; + + public MallocEvent(long time, int tid, int stackTraceId, long address, long size) { + super(time, tid, stackTraceId); + this.address = address; + this.size = size; + } + + @Override + public long value() { + return size; + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java new file mode 100644 index 00000000000..6fc81957342 --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/MallocLeakAggregator.java @@ -0,0 +1,69 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class MallocLeakAggregator implements EventCollector { + private final EventCollector wrapped; + private final Map addresses; + private @NotNull List events; + + public MallocLeakAggregator(@NotNull EventCollector wrapped) { + this.wrapped = wrapped; + this.addresses = new HashMap<>(); + this.events = new ArrayList<>(); + } + + @Override + public void collect(Event e) { + events.add((MallocEvent) e); + } + + @Override + public void beforeChunk() { + events = new ArrayList<>(); + } + + @Override + public void afterChunk() { + events.sort(null); + + for (MallocEvent e : events) { + if (e.size > 0) { + addresses.put(e.address, e); + } else { + addresses.remove(e.address); + } + } + + events = new ArrayList<>(); + } + + @Override + public boolean finish() { + wrapped.beforeChunk(); + for (Event e : addresses.values()) { + wrapped.collect(e); + } + wrapped.afterChunk(); + + // Free memory before the final conversion + addresses.clear(); + return true; + } + + @Override + public void forEach(Visitor visitor) { + wrapped.forEach(visitor); + } +} diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java new file mode 100644 index 00000000000..202619272eb --- /dev/null +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/jfr/event/ObjectCount.java @@ -0,0 +1,25 @@ +/* + * Copyright The async-profiler authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.event; + +import io.sentry.asyncprofiler.vendor.asyncprofiler.jfr.JfrReader; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ObjectCount extends Event { + public final int gcId; + public final int classId; + public final long count; + public final long totalSize; + + public ObjectCount(JfrReader jfr) { + super(jfr.getVarlong(), 0, 0); + this.gcId = jfr.getVarint(); + this.classId = jfr.getVarint(); + this.count = jfr.getVarlong(); + this.totalSize = jfr.getVarlong(); + } +} diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index d149d3c8900..1b60d7ef72b 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -697,7 +697,7 @@ private static void initJvmContinuousProfiling(@NotNull SentryOptions options) { String profilingTracesDirPath = options.getProfilingTracesDirPath(); if (profilingTracesDirPath == null) { File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); - boolean createDirectorySuccess = tempDir.mkdirs(); + boolean createDirectorySuccess = tempDir.mkdirs() || tempDir.exists(); if (!createDirectorySuccess) { throw new IllegalArgumentException( diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 71f9961bd84..c3b77679ec5 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -304,8 +304,11 @@ private static void ensureAttachmentSizeLimit( profileConverter.convertFromFile(traceFile.getAbsolutePath()); profileChunk.setSentryProfile(profile); } catch (Exception e) { - throw new SentryEnvelopeException("Profile conversion failed"); + throw new SentryEnvelopeException("Profile conversion failed", e); } + } else { + throw new SentryEnvelopeException( + "Could not load a ProfileConverter, dropping chunk."); } } else { // The payload of the profile item is a json including the trace file encoded with diff --git a/sentry/src/main/java/io/sentry/exception/SentryEnvelopeException.java b/sentry/src/main/java/io/sentry/exception/SentryEnvelopeException.java index 8c4fc700501..318aa286fc4 100644 --- a/sentry/src/main/java/io/sentry/exception/SentryEnvelopeException.java +++ b/sentry/src/main/java/io/sentry/exception/SentryEnvelopeException.java @@ -15,4 +15,8 @@ public final class SentryEnvelopeException extends Exception { public SentryEnvelopeException(final @Nullable String message) { super(message); } + + public SentryEnvelopeException(final @Nullable String message, final @Nullable Throwable cause) { + super(message, cause); + } } diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index b52cc8feaa0..c09a8ea3019 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -72,6 +72,7 @@ public final class ProfilingServiceLoader { } } + // TODO: Verify caching behaviour of ServiceLoader private static @Nullable T loadSingleProvider(Class clazz) { final ServiceLoader serviceLoader = ServiceLoader.load(clazz); final Iterator iterator = serviceLoader.iterator(); diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java index dc47b1b1fac..8f1c95641d5 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentrySample.java @@ -25,6 +25,8 @@ public final class SentrySample implements JsonUnknown, JsonSerializable { private @Nullable String threadId; + private @Nullable Map unknown; + public double getTimestamp() { return timestamp; } @@ -66,6 +68,13 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.name(JsonKeys.THREAD_ID).value(logger, threadId); } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); } @@ -73,13 +82,16 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); } + @Nullable @Override - public @Nullable Map getUnknown() { - return new HashMap<>(); + public Map getUnknown() { + return unknown; } @Override - public void setUnknown(@Nullable Map unknown) {} + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } public static final class Deserializer implements JsonDeserializer { diff --git a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java index 484c02460a5..e43241c47a2 100644 --- a/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java +++ b/sentry/src/main/java/io/sentry/protocol/profiling/SentryThreadMetadata.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,6 +20,8 @@ public final class SentryThreadMetadata implements JsonUnknown, JsonSerializable private int priority; + private @Nullable Map unknown; + public @Nullable String getName() { return name; } @@ -49,16 +50,26 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.name(JsonKeys.NAME).value(logger, name); } writer.name(JsonKeys.PRIORITY).value(logger, priority); + + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); } @Override public @Nullable Map getUnknown() { - return new HashMap<>(); + return unknown; } @Override - public void setUnknown(@Nullable Map unknown) {} + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } public static final class Deserializer implements JsonDeserializer { From 13dbc8e66cb5d18e1436021c8efb60f476a1e04d Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 15:05:10 +0200 Subject: [PATCH 29/31] bump api --- sentry-async-profiler/api/sentry-async-profiler.api | 1 + sentry/api/sentry.api | 1 + 2 files changed, 2 insertions(+) diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index 63bcf17906f..6366fccc24b 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -20,6 +20,7 @@ public final class io/sentry/asyncprofiler/convert/NonAggregatingEventCollector public final class io/sentry/asyncprofiler/profiling/JavaContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { public fun (Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V public fun close (Z)V + public fun getChunkId ()Lio/sentry/protocol/SentryId; public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getRootSpanCounter ()I public fun isRunning ()Z diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7f625bfa2b4..4bfa96f1207 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4686,6 +4686,7 @@ public final class io/sentry/exception/InvalidSentryTraceHeaderException : java/ public final class io/sentry/exception/SentryEnvelopeException : java/lang/Exception { public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V } public final class io/sentry/exception/SentryHttpClientException : java/lang/Exception { From 91e244d62d6c89951481a30f4f6c2b9c71a8f9e7 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 15:18:56 +0200 Subject: [PATCH 30/31] detect dangerous/invalid chars in profiling directory name --- .../profiling/JavaContinuousProfiler.java | 2 +- .../profiling/JavaContinuousProfilerTest.kt | 17 ++++++++++++++++- .../profiling/ProfilingServiceLoaderTest.kt | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) 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 2c6d7cc754c..7af3032f9ae 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 @@ -122,7 +122,7 @@ private boolean isInvalidDirectory() { logger.log( SentryLevel.WARNING, "Disabling profiling because traces directory path contains invalid character: %s", - filename); + invalidChar); return true; } } diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt index 86f5d51fee2..7764249e3eb 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt @@ -81,7 +81,7 @@ class JavaContinuousProfilerTest { // Profiler doesn't start if the folder doesn't exists. // Usually it's generated when calling Sentry.init, but for tests we can create it manually. - fixture.options.cacheDirPath = "." + fixture.options.cacheDirPath = "tmp" File(fixture.options.profilingTracesDirPath!!).mkdirs() Sentry.setCurrentScopes(fixture.scopes) @@ -406,6 +406,21 @@ class JavaContinuousProfilerTest { .log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) } + @Test + fun `profiler does not start when filename contains invalid characters`() { + val profiler = fixture.getSut { it.profilingTracesDirPath = "," } + + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger) + .log( + eq(SentryLevel.WARNING), + eq("Disabling profiling because traces directory path contains invalid character: %s"), + eq(","), + ) + } + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) diff --git a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt index 9bf1e5f601f..ddbae8c8f9e 100644 --- a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt +++ b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt @@ -74,4 +74,8 @@ class ContinuousProfilerStub() : IContinuousProfiler { override fun getProfilerId(): SentryId { TODO("Not yet implemented") } + + override fun getChunkId(): SentryId { + TODO("Not yet implemented") + } } From 66413a04c37120ca3077cff92e61180e7547d26e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 30 Sep 2025 15:40:58 +0200 Subject: [PATCH 31/31] ignore vendored profile conversion code in codecov --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 9b1af61b4c6..3a53b1f7b3f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -20,3 +20,4 @@ ignore: - "sentry-system-test-support/*" - "sentry-test-support/*" - "sentry-samples/*" + - "sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/**"