From f32782c7b2f1da871b623f21dd2150adbf013ee2 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 25 Apr 2025 12:14:54 +0200 Subject: [PATCH 01/18] 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 dbda0b0a1a..30254aff74 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 0000000000..f840f3d76e --- /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 8050cb8e74..03c3b68efd 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 e00d4c855e..8ff79d1ab8 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 8b183cf50b..c0ad5bada1 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 89d9293f5c..77fb5c5137 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 43ededf6a8..b6821f71a5 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 d5623d44f2..bc01399b2c 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 0000000000..7b48136ea2 --- /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 0000000000..a75807b5a7 --- /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 0000000000..71f106c0c4 --- /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 0000000000..1d662019f9 --- /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 0000000000..8cac02b5ca --- /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 0000000000..b0f93b242d --- /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 0000000000..1860827478 --- /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 0000000000..469f0979ae --- /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 0000000000..d26ba5ae73 --- /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 0000000000..b6f08ac0a8 --- /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 0000000000..6367830edc --- /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 0000000000..c903a69e68 --- /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 0000000000..aec9b7b624 --- /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 0000000000..d814026a84 --- /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 0000000000..fbdbc52135 --- /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 0000000000..a96f5555e5 --- /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 0000000000..5aad97a002 --- /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 0000000000..79e967783d --- /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 0000000000..519ce407fb --- /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 0000000000..5f0faef7eb --- /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 0000000000..6a955cf9e2 --- /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 0000000000..bc01e294b8 --- /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 0000000000..2493e3eb5f --- /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 0000000000..00bccf8920 --- /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 0000000000..b35fc0a2c7 --- /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 0000000000..8b8b2cbb3c --- /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 0000000000..6f4ca0b746 --- /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 0000000000..a7f7d60cb7 --- /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 0000000000..0724939154 --- /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 0000000000..31c57467c3 --- /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 0000000000..fc0329558f --- /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 0000000000..5e9dcea90f --- /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 0000000000..b5d42551de --- /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 0000000000..d504e5457b --- /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 0000000000..1d86714e65 --- /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 0000000000..560059b763 --- /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 0000000000..7072807d93 --- /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 0000000000..bf4d24e45c --- /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/18] 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 f6b13b6248..9978d9f15d 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 8cac02b5ca..74859e88aa 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 b0f93b242d..e7240ee78f 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 0000000000..60d9472d1a --- /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 d26ba5ae73..0000000000 --- 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 b5d42551de..f23e42848f 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 1d86714e65..65b89418ec 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/18] 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 dce5621c0b..2cb0fe2c2c 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 60d9472d1a..8c011c052f 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 5aad97a002..ecae4b1b4d 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 5e9dcea90f..1a34a7de46 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 d504e5457b..271034de02 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 560059b763..e2946f2939 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/18] 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 6dec9900a0..467337416c 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 e95c214c50..d9a88cc1e6 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 2cb0fe2c2c..87acd2176d 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 b476d483df..547811fc5e 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 7b48136ea2..8d34033ee6 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 a75807b5a7..dbd62a192c 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 71f106c0c4..ba33655c58 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 1d662019f9..022ce746d9 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 74859e88aa..c5c64e6341 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 e7240ee78f..65b66bf601 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 8c011c052f..bcb64eb556 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 1860827478..e670b46bb5 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 469f0979ae..d8f13e746d 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 b6f08ac0a8..7e061ded7a 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 6367830edc..78e0fbfb57 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 c903a69e68..47438e3833 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 aec9b7b624..0543a74218 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 d814026a84..ac7772222e 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 fbdbc52135..6cbb16259f 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 a96f5555e5..3c9dc04070 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 ecae4b1b4d..cc6f73cdf9 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 79e967783d..4e4f203daf 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 519ce407fb..e3fda8c8a1 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 5f0faef7eb..c852d0f1b8 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 6a955cf9e2..d504bf2073 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 bc01e294b8..763edb5133 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 2493e3eb5f..6cddf8bc48 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 00bccf8920..56bf66ebd8 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 b35fc0a2c7..4ae81889f6 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 8b8b2cbb3c..3bf836c7a7 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 6f4ca0b746..ae72cadf3d 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 a7f7d60cb7..ba33391559 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 0724939154..a67d2f6fc7 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 31c57467c3..556fa8b979 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 fc0329558f..a38df40372 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 1a34a7de46..c9a4969024 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 f23e42848f..e013ec594e 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 271034de02..d8aff8047d 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 65b89418ec..73f7909ae0 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 e2946f2939..7c049ce086 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 7072807d93..9c83a68611 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/18] 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 3e40d05543..4edeb97584 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 547811fc5e..fb2f8dcfac 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 8ff1d71d8e..47153f59f8 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 91d4cb7f00..15cd48162a 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/18] 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 e255e24eaa..761f2936a1 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 d9a88cc1e6..f7f7757b02 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 5345178f3e..709e38782d 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 c9a4969024..6531d67e93 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 1ec53f14eb..512d23d1b1 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 4309f06f83..bffb37b959 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/18] 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 faeebe7b31..3fb2f72118 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 467337416c..eddc3a56fb 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 f7f7757b02..cd7c34619a 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 6531d67e93..512801bef1 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 73f7909ae0..14a4b96a86 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/18] 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 cd7c34619a..5ea7bb158b 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 87acd2176d..ed14324606 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 bcb64eb556..ae3f339aff 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 d8aff8047d..ec87207270 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/18] 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 69e8d02c89..6917c4fdf9 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 0000000000..f5f43b794b --- /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 0000000000..abd112537e --- /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 0000000000..869dd47738 --- /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 ae3f339aff..9f0c807dc8 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 0000000000..7d792ae8ff --- /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 0000000000..bd97c6688d --- /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 3d8f9ffdf8..abcee7cd62 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 4dc477f4a2..6a5a2182a8 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 0000000000..9b594c2dbf --- /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 5ea7bb158b..40876f4d10 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 d22ed360a0..98ffc1ec0e 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 ed14324606..6a3e5e9180 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 0000000000..ffc779a02d --- /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 0000000000..34ac31c66f --- /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 0000000000..ec09de6075 --- /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 25f0b07432..0000000000 --- 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 7081c58fbe..967f9e6eb4 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/18] [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 10e4cce0f1..bd829a66e1 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/18] 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 0000000000..0f4628e3e0 --- /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/18] 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 512801bef1..847ed8830b 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/18] 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 f5f43b794b..74bc5a4654 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 9f0c807dc8..f22eb76f70 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 847ed8830b..c182c96ba2 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 abd112537e..e721260545 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 869dd47738..f3db84c55f 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 0000000000..8dada3edaf --- /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 8d34033ee6..d4d8160048 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 ba33655c58..7990e1b3e7 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 c5c64e6341..d142625545 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 e670b46bb5..70f1747fac 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 78e0fbfb57..7c7fc5d8bf 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 47438e3833..9c9ab8a873 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 0543a74218..f552f1fa81 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 ac7772222e..127ce9a626 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 6cbb16259f..d5971b4802 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 3c9dc04070..c71787f837 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 cc6f73cdf9..abc9a0024b 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 4e4f203daf..bbba06b8c0 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 e3fda8c8a1..f0d7b9d090 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 c852d0f1b8..337cbeef56 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 d504bf2073..f8632a21bd 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 763edb5133..c0cc52924a 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 6cddf8bc48..323ffb327b 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 56bf66ebd8..23c9f7aa29 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 4ae81889f6..ac12de630f 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 3bf836c7a7..9bbbea38c7 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 ae72cadf3d..740fbc8224 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 ba33391559..9fcf776ee6 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 a67d2f6fc7..eac63a518d 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 556fa8b979..cde4919bd3 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 a38df40372..dbec70770d 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 dbd62a192c..0000000000 --- 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 022ce746d9..0000000000 --- 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 65b66bf601..0000000000 --- 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 d8f13e746d..0000000000 --- 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 7e061ded7a..0000000000 --- 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 7d792ae8ff..a59cb70f73 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 bd97c6688d..5f39755545 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/18] 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 1a2bf18674..78e0dac4f4 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 4edeb97584..c2ba950c5e 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 74c9b1548f..ca58bd7948 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 bd829a66e1..a1a7bb3da4 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 95b540e817..9f32d8cb8f 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 6efc63ebfb..8890af5a32 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/18] 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 6a3e5e9180..5d236bfe2c 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 cd58b5ae34..026177dfc4 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/18] 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 c182c96ba2..dd8db6237b 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 0000000000..3c895fd7b8 --- /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/18] 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 74bc5a4654..3e11247dd7 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 f3db84c55f..bb78af134e 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 0000000000..5bac6d3ddb --- /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 0000000000..a0963aec10 --- /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 0000000000..885cb45e41 --- /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 0000000000..9f4146aa9a --- /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/18] 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 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 diff --git a/36354ee63d9240659b46ca78579a5c64.jfr b/36354ee63d9240659b46ca78579a5c64.jfr deleted file mode 100644 index 52073814033e0c800a042c1454ac52f2e27617e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53647 zcmc(I37ix~mN#8VfgCF1&e7Q*_AV;BGedXXhx5(uG!1Ag*U*i+C!3X-)f8P_)mBw= zxL1hx-KYo(XfYy!Cj!n0C??FPD4-xJDk_Ddg9m~$>Ui`0Bl76X>dfjY0SA9(S7yA3 zc=6)Jix=;`h!{71N}oQiKJ4GG)IXMIb!181u3_GS@v*Afby@JN1@!$Fw?9k&RL%Px zA$|HR`1Hl4E6BJ$NAR!z8W-0$lTPQj6WZmRAPG5vbBjqKo8|hZgm#(hA4{i`vXJ6j z35lx;aDBz;LWZjfa_)FCE#$bW5Z6yicgB)3R~6>^#^&VkBf`0})6*HGMma8#%5g5Q zhwGb6r&{sL%MFWL1KQJ40;OlN+>i!Ql(X5|3Ef}1@okx&kp&5r`nZ1D z?|$hlEBK^I&9&;MTJ}CLomF3(ByMnb8s1Kz>jQlec>WhrY^dAY$XZ!N<&%N5tbK6CxRD(bHh0o!?X^~Y*T}<2H>A5+dZ{|W(iv``PA~lK zH$5Rqa*FGBj{5h<-8s)sWOAKCvKj11NfO0C$_W@qC!%$kTy29SWx$1WC`9NZOqsmB zJ&^;M#-_jtjrvd$l>P(;H_4DWn4aXwk&p1wrv(DXLFnvl4L-( zvqR3Xt~-$;JtLl}8#_~mwEBtKf3=fY6_8K0dOn{A=r9R1%ESGTagf$ywG#w!Iv7V! zM)^i|R7HEQ1?M)`FfT1ki}d>GMUDxL0g+r6XjVg;~PfP=K8oFcc;`QQne|0RxL|Zg-t_8M>3)2 z;|76_shoz$U`7&6gmHt?tP&CD(v4kH-t4QRcieXku2smz4h^g#%^@OJJqY0XU&8^=x?J9$#$Ib5}dUTr#F ztM#Q?8#jpJ+c-3!V1$ekHKhlW{_^SO*CobcqU%N270keMp3Bv4J`eU$} zSX_T3tGr9o*{_o_Iw}~I5X?U?-VJ^_#XzUl_{PMd+LmTHM~O#U62YjnwnR@nK^=Bf zNzgv2>cffp+xbilV#Q8sZc4?|nRc~(C^pJbt;76rKstLqv=};74n&gv`xLF4&7hnY zy4uOKw=G}Aj!v$BmJXs^mh0cF{cxjOFdMrY*`EQ^XQ@06b?F=-Bh3;rvL-pQ9IFdm zZzOu&TsNvu>hORK`YBO!68 zU=CnvV-x2498)DZo)WzYawCCxo(&)XVMyiXfF-;>?k8HBHb&?yq@}jW067m2x2g9h zqT6T6;}fxrkYOeGxs%jypn&?Z{}d_}F!>CM30YZ}NrOk@#ZVCj=F%N{%Al5XhmpWo z?Ci#KWTB(E6O>ge32=kyms+D8-=JRoVX%g4BRRsH2TJHymI0m6nV>nDIVObOuJNlaQv&aF=R{h=Zz`QKo(V8v|}UsHzG@3LBnXqlxg99!>~WI% zYH)~A$5bfettoWjN&2r^l~IkFCd(NSZJ~WP(Db`sMsB6DsUOu{pr5(r)U>J1+}Os=Hqw{ORvLf&Llb9!UQTau23|PIR9{|D5a|LjU~0eG2{a zH|`(OKR&I$Z9et~J{ZM`L&^NE1=A&3A#_?7zQC) zW7t)#*BI%6k&EM^?z4XJw=M?%|7cmi)Jy)yK1ls1_pkKS2Aaz0si)JLM!Or;n#Rzy zv95k9hT~{GXS>d6I#+!^kHU_3pVJqgC%7i+=uD!SChG+FHGP|+zcs6Vxc}9SkN@WSq1v8*@55KQ{)0ZI(X9We ze;2IZWBPYd`|ff{LuHyHuBWtGb4=I2CoDOD!VLGBYA=8&qNy1@HA_=-?oK^*CQUW>A;-;f&z^I!O4k4C!*j0x{-6K-KT<{EE~#?; zP(^8ORUgp((yF1CA%WvAuX6oN?-+0@$6Zn7YScem$w02Ea{WsGa&;BO;+iV=wN-O2 zK7n=Mb*#Yu{-5hhS9F7^q8nMcJ!|T!>SpkHthk$ix7v+2qZ-zaM!(!*q|LXc0mECX z+zWtVe}>`0zY2!H;wMo%qa8Sc<~svH8|X(C{r5qYVJ?7v|o zy~`lmGM#L6XaL>2tK7?h?m&j_J>LVmD@^FFWXRoXwEjN5_4l)c2aJRVSppdNp`zxj zDr(NdRqoYjjyfLJAdwD;wN?N32+QX%B3#^Wt6T%sq4GOc;G+zDo$+FWmL>tp&? z(DCsq_eO(`PawgCij~r}x^=O?iNTr1HAe5|DqRTto~8c5AjXrdbnwViEa7RRZ?`af zAoh&m@{Ezm5CJL$pEVM;no2`EwpF>e8w7l=!gf4wZO0DwI#h4RP9{fQsB-T@1$`OA zy{J{NmO(mp&r1d^US?(Nu5!PEGE`agD#|FYmtQlA!AxqIDe<WF9}C(?&~0Cp`z@pY-^O>^|NmP;47<4f20HJs=D%ya|B<~PFy7x|@9!J$ zAF%fijrW7>{Uf7K53%}<;c^%W>P-GIBj2B@+t~euNbUjw^|wgYhbC0{cli!dHVj=NI7Co0o94ieThpK`Ey-;_`YoZ zyVP}=>vC5~;$Pv?yZcIHW8KYR-tEvQy)4armCN<7D&ekXrCsCFQM^{mqzyHoa-GY4 zJy21JdIORv1Kd~&36AS>xjOX<=P}TmT>5Ca*+s?kEiU(bqtsi=DYg9B1jR3Kxfh~% zbs*iQ75}|Y>bHZe>d;%nP*4>WptsoNu9>sMRfVnq!%=$N;d1{s`@+#LP3O{Yce>np z7yZaY%2GzR-?-eDR^gkTz}-Comb=`NQNTSt(^t6M(~z#> zy;4sHI`_KV|6wHDXG*x=<$jb_sb+aVPkGSgddRiP^{{KTtIv6DnlrA7J4LWsg8X%! z;&0SMO`Uf1-~aVldC$rN@A%X=ye}s3?IpG1+OQbc>^&R`nr}_(OMPyA7Fa*_6R~&{ z+xpyU{S0RyY(0H$FZ#+%#2g2!P;oI>kHZGrqnH?0c&R?OSc~E0l+U;nstCUy))I8r7n&EFabw`;gH93ryWd=T0{E!k$k|K zd_r6na-GzsH4#On(z8<2=+eEmGc^l#gK61ZXH5SMpGt@Ro7xs(9iX7ogp>&1XKkxE zjXAQiwWH{NOU(7a;3%gA_)I0$ExE{=cmiCe@WH?$TII3iix=gyB(@eME;JV!=iJOb zj}q@Nf1lo+$sHGxS=o}BC{F#2Id2;`SdwF%t*SEz4rA(JE$WgJ+vH42hMkN0ePAeL zb=-6bShZbaRc>xoX9_;Ovuei(84$VF7pM!=dGxpQSbp8!rzM_MdjU-16NF5yH(KlS zp5+anG{vYn|qSTtcYp;pei8SS$(u*c-&?jrEX$Vxk* zJ0%IW91Zt<_WXoAs}BB6AvGr5E`ZEN{X)0vr%sUC6RF~&Om$(cKwWH_|0H&$jS@qQVpRG@Jwh7r-5-!wCy~&Q6t#1O8CzFYDYn#T5 zZK$1^#F&Kbs}AVYYX&Z4PQ>cr2@q_tycERpiKO zOmrH!4xol*HX^zH85uzvS-D^-^+%MG7%4zkYA&wp5Fljjda5ofwV~hZMOe}khzgOj z^^_9z8f%Y&+lO|}@90Q25cmNjmZj`ls%+R`yjpubrC&qVgDddGx`a&A^S z)8-7I8q%H9JF^+aOccYj>ULPjGJv5yJ&i66*vRVl()BPpQ{Qd2Ugcix4;xGRZ;hdgUPr0?J&JZJz!`tn$2t>9sh-uZcN-n?cHMlu zk-$Wn?u5xVTknKHU28^=k}?=goKC^gQfE@mj`G#hA&$Rw-uiT=RU5}F_=7S6V}AI)Whop8#)5L#8xMJ7v7k2`5JQ63=L-kp9&a!p z#bPDP8pX<@0?3Yr$GQcAhzJn)vb7e3LSexx1pEPy6bj06I4;TF5ZZxQtw78#`-74Y zjR!*^+ZO0$eZMW3CC5}xq1J?8&>QiDeBo%=BZMSxOqAlH4C;H)wt(af$>E4cmb@O@ zCK!c%?`_bq7vo+rBFE)$SQ5p!&*v4yIQT}CqoU{wh+%I;^oT+r;Dr76i#LW-$MlPm zI_|Ouye^;x;g~lR6Jr56H9_hqD~Csi#6=y!B|)b zMExEgA`tw6kRSzOQp6jI_`{;$^~M9fP%Iu3!?x(FC4IlxgID9p&eJ3Mt@wnd~5Bma=6pTn-Q3we>f6y0|C0p#ZlD=Q`g>;8b`KVtO0}^CRC=`*v_2ED` z9Pouj$rl#l{-`(T6Fg#6u*Dt4922|+s&nOL)N6ld$pZAIdT(8{&PS((m?-!He#s-m zJyI+X4o2cJK@J4PU_20w2E`ZzV=NZ(`mFg!>G{7eaXv$ei5gV}e<&Ieg_y@5gB%p_ z&mR%JAt~;S`9oqPv80B>DA$UR25`R?1P1qhi^l*U_U+%#Rw{VZUGS$6_%_6oMh2FBl4k z;<0c%Ap7HfZ&*Z(bTBUZBR$bZ5yt~(9aTD+hU$D+gZQZa9tcVSQHsdHxa{+Ly;9s0 zh>O0U=!uCC0}{l5p+M64ljS_d#H5Qv9XqyQG*$;fXD+Ss0<1Zj+?RZioypV$mShA7zwy)YBI1I_(KDgB3cKc=g>s^(AiyAmTqTfeCc?Du(t zAx|(85rT3Yi#I7OLm3x5k{5j83B@tgB&K1D2kDw810e&cp&L)pO}$Nr?e3!2fThMh?Q7Gl7x6jh=w2~ z{QhVp90zd)IV3|RkOTJ18(Pj$kE_x=&^}YtXd8`r1<>pI57w&TSX2ypJaJzP(h1^8fVsoxiwQxf%vx9E zW*-x@G;2r}MpkNI&D3jpvkyJ&tM^8*9z{0`0Z%L(j!B-7C*}``v1r^Uh~QLAN^)3? z2wE;!Ws&Rn^eo0FGoh$64<H^eBj zMjWk@N}Zb*3$tf~9nVR?cadnETpz6S)_Lk9Vl;$>yXXx-%*4e2*6=Zp591d~XGo6w zq3c86kBCOTQ7oSmOIElv)`lMIl<&c*!rZ`iE}Y}GAnd`*b_)PVrXUt7F#(e(1Zh}` z`+XkR|HM!PODZ`An_w^$3|M*CQqb|>-5wLIHnYZj(4DbC9G10M#2<-KxglWr>V-}n z40^m`5Hbu)0iE|*=N=35RM~O@1{Z|DwCecEVGJiB7LoiSW~`9l7o*XL7hDZd?1_ZJ z7y+=k8uNffLTE)DHGOrDc=w(ZczIPPdH1JLLPw$=23MF|1CdxXM#XQ;6ZOV0^G9M{ zKkTo5ECE7N%papVtAI73eJRwNhyRYLA5HI1x)DvEV&NBv$`D+V7!60|Fjjwp7jnbn zk;2{}gtAW#3E{XQ%V^ePGG)vW+LQI!lz@F&^cQBy`Y0rqKLq~;v{4M7coch0{czvG zKppoAaU_UP`Xf+D^lWT$JvtJVwnplP$Y|6@%{_51JU~!8;a3qtzF-VfS2PlcNBkj} zszZ`5R%D4}_VeR!FAeKx20~Elv8OQ#O#}-Mte4^Y5j`HPon*K}BXTGlg%==PgeFX` zVJwp2D{#cMvRUD1Y?$|1;tgtOoCY1wb=@%nH0%YzHC&%f7EmJ&B!L~C-0AI!*@%0B zk6iwFt=!(3L|le6N=zmYL-6z3EDq*?e0joBz%Ulu&?9~Y8@wg#);(nsj#@y76Si+c zjb`?VRhI0hI%QY)lDfR%hb-_X>F`d#l>akKReY7!|twx;I50X$x|l=;gNWg=efyhP5f&_)_t zhcr^yaj3d*=_g-Qzh)`5p+u>4^ETb~X;&^qR!t>^Yp&+Wnx)tBZ(2$oTcYG~*i(w& zF$8znmEBn1olcaGUaMA?eYCH7jivOOdOdOr-JlY?ZnWMh?<)MVb$GmI$uhYZp~;y9 zY_GDZZ>BbN*KCJbL%kV-}mz0$D)%AoU3$u zYtAfKsy@58YSZXwo|TB_rZzcqs|kPUSVknZ|uAmXM2Yx0rPJ@$CHD5pW{EXp!n}4 zP@I}d(6d75(E;7ZGC0uR9+ng!3CZp6ep$_QMPm~GiCI*~vh9e0*Cx-m*Yh7P`{R0^ z>9z*^-<#o~OHEVNi)rl$T!tHx#k#8nzQQwAo6kSn9vj+JBoYO3 zo!s!ogZw^A@#pkjd~@5J@$@Vo`||ZvqP%ycn(SRYmw(GrK~r%BreR8PJDYkuN-~8G zK?*2bdH37PD^PssVS%u+L+h}|zB?VN>k)q8Rd`e^K%3wNDLuoRAqY9NoiFUT`8l2~ z9SqKBEJ3KINzI*^IF6#TZ-tRe@>^H(g~xZUKQb3_F|0?bNKhVC_=QUzQ~2_QGZbWfxmKELHh@HV`i`0v zFW^zJ2pThPN^i=h1%woyevnt*+x8&8)_~O~?bnV}UwVmo9Xq2KHM7tWGBNvgP(!^5?3|{OePSeIwfg{H*b81Me>wJR z7F_Mz1>NaHxo06?cwzDFeEG}BW^Lz%JeWZz;njpQG`9gB=B7gZp3X-+oy zoC{{jB){n0>cYW4zE{n5u^KXSQZeEt{u-0Wwl!p839QhZNmK+x@=I^!UtfLKt(6b{ z37y#2mrHkw(@U;hdVrC_Uv}{M%dg(a|JKqcre5I{vl7rQzp`Im?!q*>F=a)Znr~>g#d8G6-W`-c- zwQu;YoALOIg`EFU%qcxt9LIFA!v(SYWSVp~;h;#{01{=_B3}9OrA2)CHZHoY_U!TY zoUf*leAmw5pD(**=kO9b?(Fs?4f0X7g#vW!>*hbcxN;p!75+GLZ?w>AkCPdKkj3ly z!lJv@m(sp*gsNyv+~lU`tJfZ0{(Pyml=!os8F~+SX-DI!B?S~k@=oQtNZM7jB$ zYH~G1`ui5z{JIy~v}CZoJC4N$&J!q6g9(Z#d-n_{hY#-=UOpFft7ncAs|AE8%A3QL z`J3Jx{)weE|5~CoW7I9Na)#N2N=_KIpd^p#o_z4>H-{Iy7o5%Xtk0V!*%MAnBzde~ z$)U$q@#UARop@khu9ip?*v$%WTyjn2#^R&FOd!C#kl_eu{27nOEGUd{$gZAtp*MWzB|{&34G*XFvz2g`k){{1YQf0Y9>!y%rTR0R zs!wFG`Cpu_=U#Aa%@?08zP5&WEseJSokMO_qfy&U_AVH@c0L~EWmPR$8p#%aGj!e4 zcYIT3)=AAc{8dNz(21tS-JvvI-5@(}1=Tj}|aEFZS~Ydp4D8sK-JxV z7fv>)ux-K6yPg5hmN#ssjmADM811LAyFawb$)^iOe)K#Zn=O_9gG1%hVD-ip{S5pW zbF>l)$o%zt^B0bM+EPNDQwi#A0XUR}9<59TlgdrX_Jt!?JofRzk>w4*X>ul$N^5!Z ze_A+l;WK!Y$HdccREa)x3dlts`t!n(k6W-m-3fboMlNi)8qUb~?z4E5i8HnXVwZ_F zdXR0$S9p|(Gd=W3%St|3IC9I4csSWTtlXc@>Q!REkjoxjT2r2Y#zi8AnfZe?L$|H{ zc+JrA#8Y-ocA5#_JVo_EJjx?eXDR_g=_E$Bu3dPPSV`)^3RJgIiJSj$6Tf3se)D%T zKEZ$Cq*G8;k0ldjG2D+!;e#w;@uU`H6(O=<8?RisVH=M-d`zJe!@JpxIiKj{%Ffcz zTfbl7*Y0~q;T`GToeiNQ=~QE8PDf5>vHUJrShv2c@Q%D~&fNh!S%x0fqhb7}!gu`{ z4@YJ(W$u{6qFglM?|};O!5a$i*xja_IDp@bvT^{6smr={RQF!1WSs-aZ+2AYpIft| z+KExXl-jvBKvQQga?{T0wVy57S?xHKj9iEdY;Voz)OkzcSKNU|@BK+HNzi+QY*Y9H z@8QuqX=sQ)Za>1MBwCgESJ(6VkKoaJUV6gROxfHe`@jjG;n90ul}yuwtS;opo+H(- zA1NHEc2ukk5@~sHUX<8o2%YWESHHLUx#!F1Q7yZuGymEh)rSwfxTD%}dMmD2Gy*pC z+R0vpUwlhJ;h(aMyHPgdPOshK|5Z7E0gQQn(`_R^+_U*MXO*?Md!$r0s}H<9#?FrQ5N~C7u8g}ubUTRFEvS6k zmOtD!vTSN1gq(`+IK4?iBS!%uWAS>hTJJ(q;}9Cnsbwbloz({p-nX-S=5E(}0p71i zpWLdH--+$)2tYkMMC7V(m+q)8nU*O@?p z+4I9A>kd^b7G^Rj^2JP$MSLT5T4}ue>S~@m^V(`h*P}{vw&-LfO+$XT2QEfDo>oKw z4DiA=Pu=FM%J)D4hryu`odS>!TP}xeEPFVqry}arAK6iT-BUO0tae$j)hplF+UbF#quN{EBJVI8Z#uBz;7+i&z z9{QPwv6U3iMXN3R$s4sm;hK`h9Tf3wVtgws<= ze%~Vgu^syt@z+|=$Q&y)8kzr^ncVCNVx$w2KUl523T8Y`6rm<0$uVGAPbJE?^Lb@1 zT$jg*B9of+v1;Fk-RYzNT`Pb0eH;0!EuGVGtU9Mz7Bb>=M=zt1ObY8Z@nq|+O}#~; zn)t6GW^EX2Pm6}JgmhicUv=pI>-h>C|0a28U#MFJlrc_!~#9md2|m9Le9ml_&c)Z9QJ(YQ}aUSsI5W`*;{j?5S&oAq7Ymr3=BB zP7^hzLIiRqmm>5T=ai3l3KD2`*g-@^J$^IL!ufTIua@x@n zi2`LSzXrjoz1u3-nw&Z#i3vsFhL8F-?q5my;c))zmXv`2$_gZskjahj$3= z-N6~}Lpt0Eb889s*+Rw$fhfGYmA~$R_qUdckEF99oITxje5LR&{vLDRD;Ader(%{e zO(!5UwwF12)L9}Sw;kj^Kk(+k%EM~9cl=~~)AaQ%LSlaG!fiW zy4ES`;ff|WqHKBMjIZB#_K7np9+ukmlfBfa-chLo5%Q-e&seqOFHfG~XmGUnlAGBY zS)V>$T`=kF$v7&Rb%0uwM2ssVd zs5|R4dHKNgymA?=AZ$CdVT$;%*+_6vXF@kZkU!lxl!%Ro@Rg<0%h*7sk0;g#Y%+><#_hYw7L<_PYfgROq1)D;y2(;?_wF65s}Ku$-02?c z#LjfxXn>f6Rl6Qp;A~?p8pzmVoWZ@?7pnoPDuD|xFBnN~#;`7-LZ-C!x@N0DC+>I} zkBUQHCN=l6Wyl0T^0zD*mfv>Yl3^8_L{upvbUN*>=uRhvmp1WNz5V8<%Hwa@;*DLt zqF-HMJNSEmk^I*;)O0=m#Eq42cu<3zN>*n9!A{qkPAx2W>!wpH-T^blbp4DyPn#h~ z{?9km%)K5BD>1kk)f+VRLF1&kH;i2I;))we@m}m=dbWwa%;dKZ!8rCn!LDO zs0-@)*5<9evJ;BaUo3;HjPTs1NooKp(_ApL9Tia=LNmQF zN}HeT>E9EMD44tM-?n1G(8|rD86k`KetP1YoGCeHHrhZEqPetT zl0LGY-*y$*@Evq7O#GdgtrDWM=7Pva6AZ~OTf-OLzk5yPK_T?6b=C#;oopl%qbx zkCUDzi&{SCY*}|k`CDZfjY@9IG((U)cC8h5EZ6MCBMvqM_Va8$~_>!#2XDQ4qhiD5c2$9o;>`~ zUY=ceYFLHl9Ag~LX|mB$OvjCNrxOJ`@5s|vUdeY^s50vqV+0YC%F)@yo@b3@QuuZk zuiS??=(!er62};yUyrdz%Qn-E{pT7@Aii#~m4!XN(RG1-1;g(NE>_3Rd9?RT0x|k1p&YhE59FM?ZNkrND z9G`!F&vX1z3xi#GtQpKv5=}Ag;M7(wQ*aeGjI??HW&x)>6z1cUf)Y_<(`UwYx%%+D zkqfVWbYA7I6}n);a=P@~UIavxKQBCE)k7O@JEOeKMQEBln{HBqDykt|*t}rmmcvh$ zy*-H)VOiW2mSLaG-Pl}I@u33Dn0W5wu?}Y>8OcOhxr0Ca?)n|2JhZrNS;%#JL0{O; zD@$J8&i7*8Hi;l@H>>@ivvA2|V^39n?Jj=HdmDH0<)1K7el;yk_q&)({Fh9`xqR-d74>snoT@B}-C z?)*fyQ%KT13nnU?l$jzR#A@JR$c@lE%0FhKY!*Iy{8Wu~^UHP)U-j-JJUa#cJ7l!ZHef0~&%Rhmmf=W~~g6301OZMLe z`jvk=M%gtroYthy0&>?MHbyO5BX5+_&>^n*;h>Lwi3Q~p}h z!Axk+^CmPWOh#Os(`r*oBn6z=LiS)EUiqu>qrj(x%?JpwRHA(RIInC%aNb=O$#iw8 zrNL34G*QNxw8@g;r9jOBMU=}o@cFwIKE~f-LGizeg{QeQ=n< z8X-_qlS%&O$9eLHJ0Iuow%~N_aU$CkVGdifWZB+DdtL-`RxqM~X5{$?9^-GdAo)MX zhj2KEKa+9th0sn_(@6nTQyvA|EwW&B$#EjxINTRN-GokFfyvDV1W}$mI9&Ptvj>NN zKZ9X3&YzgxE@TkCkajq#9CWil5wh{%@IoGV_A&?c)FjxbWv3cB@DCj`ES==B9U;FG z*2{{OG0mei0b92-U|FZ=#U5a!@Z#e`^Ox^_d}zhik2b{pIz*GPB%ExwV%UM3zFje_ zVm%MK>J$M-<|8cBQR^{-5b^{9Bfr3-0?YGvFyz6`;&kF8OCsbWtd<|ZLbF70pV--v zHG-3s2lw&hlBf1naH80GawdkmG#tj2o=V6|`*`I}?0r~cSsz+1Rlw%(baEyx%F&br zn5!>SKHXc&5|PZj>dw?9)V534yx#`z#l$60HMgcP3Rm4#i;^B-H<(Bs^9 z(@dhb-+ruF)?->N(ClaH_Aup&r8S>FjYo+nvFR%2ql9cqKan^OTg?5t_iX3)TktG( zG9T6G#u$6z<916K3kV@EFXG7~uP&;5|0NdSINPJ46Q|bL*3+F%3fLyI;;WDMRWNf| zu(Yqh)IE*k%78Bd!olo|a7;S(IF>jSS;UqmY{c!hkzsTE;xF)c&C(?eCFbCU^T(Aq zc~ws(`HyxDS2k~Wu{Tw^=SKltnr<*y7fJpwf{WZ$fTlUu+u{0!J+>oBt zHDPeWpDfWzS;I=jWjFxCI==h#!n;d%pT6EwefP0!SnspK5f7MS5mmZnA^-SZKL0Qr z1r^UMqR^Ravu7oiL<%_LzOV=37rn@Zo-|>oYVLgJWkWF(;uhb=4K+BUv3zc#rB7%Y zQ{sSNnu>_~)iwDqKU-Z>v71AsIz0u;DLT;%LCA8rZ04g+3YLy66=~BnX>_tv&f!uQ z2gzXqAmq;<@XEZUAM(AdFnjh1i>4Vn8|nluGjocFE&{^cBd~Hj@c46;d!SnA#R4%I zyO3<9p^;3Kb<57oZ+dFkncr9_CiOn@j1e~p!NfQ`ywrKi_ORGj^7IID@bJ?kDvl^^ z7D*+lS@nJ+^(Al5m8&!aNusg%Kw^>Ch5K(@+cZ722 ziM^W80tO=u#9mI@^u`=9_HAphz!2r3*M^dL%4?3N z4O6LpVY3jI8==6r!lW}Amnq_iZ%t;sb^XY~D|nPRi@{0-HD4a92=coZ_t#WlCF$id zqjCDeHCwMnBigqg?U^~Id9psK?R=CcuOYtY8jB_JqGH9$RQRzASRCIlxuNkq4U@vs z&3xC@IBjE|1(Vs|4<^`RE@T>#tuma%I%%+*rLY!;_Zut-b$%}hHFwZ8v^kIx=XR{U z9wFL)4Qy~~vINB$^PWZ<1t+o|_&s{;ufe{fCr)hE@F2_A@CR-M5C1jTSM4L6BG_?u z)m^wdu;Q&J6Pg^Z*+2qO4!w1H*P)x=tvuEOhT98p?r5t+xJN_134j##?&9|!D(tFU zBWjv78Kz4djKwYrw4JsrAf$lkqRV!nzKRX&W16YPgNsI!CHI{403!t&Gjkn+bSicj zoYOR>WL@3qM0s}Ysbu-~wWn6h04T&>R3ibw%Wu>ui*~(H)7#ldJs0!l-4KYR6J{nd zITpUac694bY1=LZhex1}+rZze1 z(t0XUF1w1glD~8L zi2S2>FCS52&EGP9td8!^8%O51?7DH}Uo2LKGIrcIvg4iMF`}nqpTmwmvhQmfKth(S z8(!FkM~TCZ+GSCQ>9aeo&3*lT>~SvP!E2l{uDKJ3r|DU6h47=h*S%W#K6ENfCXS!% z)EbsZ$QuZwxdR)aODsHErVELdbeoJrJYa-W7aj#1I{5is9Ar_dCDaBr4iO;Q8t1k3 z{Hk|f{|>u_slS~X{8aOcqo1V*7|G)T()=ShIP!>vEz0qnODE^jW9GA#FXtuqEiD>cTU)fUe>_#_Et&4V!w^_Pk>U z34i^{VTBdS%3%*%NNu^Pcy<<7Zt5OZ1$zn)ulQ_hd}a#Vpa1>CEQF&J>cJ zOx0_^t@RF8D?3PRgIH9v;t(DsPWj2=UIEk1euz|9@xYgR`SSLMQ1;igFP(h2b?KEW z@K7w|Go3$f@q(BpQcbQcn_F}Ek9d@Kc@5jVIw=K>P1l3fVw9(TJN&@P=YKoAVjDDV z6C&f_Idcl7Fhh|1jklhP=(Jl;t=LG|JWji}%zls?$)xbmQ)lFl?0D*oichQRaW^YP z-J!bT+g>RsQFedD@3;o{xfX8`pzbj2E$D0$vbbCV+k@)svQit45~&x3l$5|^fvpEK zgqcFUH;i+9y#7(zRUh@p2v-%|mM~+capL-ow+|uY^*e_Y6Y;xOL(5OF3kbEzZ*>={ zSkpD+*tG;zBrx%0I@yw0t7apuW>eRYeHQjVr~c?#0z&HJ>jhfDD_uhhmI}^3x(Y5% z=trZGRr65(Z|5D`@>9kwOJq4Mf90_*A7Lt52mGFv|Ma|L%S!U>T&lZ@^|Xo&^NuYm zVeH^u5;H4pqt$GmH>B9RbyTd>UClH!N+;S*TGb2lh7`|G9ht;T4CVTnLZ&{RrrQ9s z_1B2?_QAR*yh31&y=NZ7mguS*Yc3m)QzMfn*5@wqd0RWD6Sa0JjIJ7C~?omkPXI zCnOZQi~A#_7a9+=4z{KjT8sOq=a+h1G}fRK8db#RG6Z;oUZ`s=E_Rn%OI_|_K``j_ zf@ZL_uuK%{c0~~>GMvWg5(^zdJ%)C7x78tfP@qw$Z+91aORdGFz0m|Aq11zX^;wn_ z1D8tTXC$2zv=bKt?hjT?SejG6fllU|ZzqrWF+5xQlcMScbe53la21KgUdkFoFVtswQy9Kpp>iE8TBrpcAK<1Pl`Yg`)afBd z)Gc%si#^MVY+#*w-4RX+zBHf=ZFf7wykdtqRA|^f_?jZW2g$=o*SMltN~sFL+#aE> zLNA1C4RAReVzE%Gi~QTXBIjc+PidL8FdytlNn*i3Dz#!DH9+e;r71ZMhX-6phe9@e zWKkwBDsq*AOdX5C3AxH!3SL9t4MITquBSwyVFeU>M?q#m)BZ079rJ~HC|~ppmWylJ zC-|Z&t-b=nGB*gpu-7IPZBa&17-8~<71=NrYbn28dHB}~ih(f5&bf~d0Eu{`L8#q1 zH-)_<453j8EPPvu$L(|#ik=|jcX#*iE)Et8k;5n?;w?pfWnA8IsAw;vY@2VToGl1? zNRmWvS&8UjUDtpjJ<#b%?>Jb5w7O01zm(pr3dpAvC7(eDbeIH6(+N!(2L*d9rH9o% z0F0v~qx?{IR7HEQ0PM%Jq&_1_P=pEf+^iBNQy{+BOHc|ow1#SCjL;~oJ{D1@w9sl}p+s77 z3JcyI3G-fMrCygqRB$&6w}qAEmJ?;<*)XTw(8FEqDs_807MEfKqrIkJ?*!vh+Jh>C zAWG^u$~^LHQm2CwLN{az^^4>ZBFERZ z28U-OC0-e?4QVrFiuSg822l(%1(ld*(e+A@C_3ay8?m1hnjkt-dKC+`OTj>BzEGdW zF&3h4^?K&??3Ld!w^z>&`GTgbxYSiB)MXHTSvaXjUia>KL4bbwc|CJG_UqWYS8f*} zDTrQ*+nEyVOSv|o9>q7ecV9Y_P-M+Q9lWt=Q)t`^!v-{@!(y1XR4ihgPA5l*w%P*+ z7xjez5$be(tVaq9fxH$W5%EFjx-=yiu3sdNngY!6g+hbi7)p^}+0f0gm%0XveISE9 z4yq{tnJggg9ad~lQ$#>YMRKD~g|7YsN`o=LgU9?xCobSC_dA`qLNB$7tV44!>(}mK z9hyP|^}~rJ)JC$*yEL7J8hR8|Fe)LKn}ps&{B%nQos>`<6N_?N^2Jh0Jlc|Mj7oJ& zl*Bsnu%k*sFiKV*8Ypj%F*S%4+bch>*y;8Z$?Ze27J+IV;gN}M?_{HgfyvmO=dDaOge^v(z+QFFk#-1mC9ym zS-6+D9KtP_1DM*Fhxxvgsgi=05zvy~w3g(TfwFQXCZRr!Nl~Ks&bi9WXAlx-l915f4)$$;$vdp^i34ak zOuMPC(sk<41jq|$d#qpa!D3KM3SpBS>+JB! zjzxqtitbh$@VcFHmAvAqSkR>BRIF{7=0i1*T+HtM-5@fPek{g~Ne%Nc)!89Sv98IL zV^UZY>mLe7>xFVqR04fD6bzNiqDruis}l;b5lM4)z^X5&$PFeD>d|mY@P(=M6rhJ# z1e6r^XehsiI3!d@Unt}Ki_wJ*l~9VzsG*vAiyk}LLi?_6Sh$u)>`!G=EvmafKhucC z{rct$iT#U8dpVt65erEj7Qckk@mQ{EYH|3ft*JwQ5;b+{Pd!b2`qMzukp48%G^RgI zG`G;7TQyDTPcuz(`g5BmNu%ZYeN9rfF$>b2%-YzGd)D&VxV zG3-R^>CAV@>ngu@qp;mIT@n!8gYT)J(~D;6tq|Z*`qoEz%a>8VPk}9jMR90LP7Q~wB)~P{-#0X-}$C;d;Y;`3I9*}=tr~uONqCt<84a3 zJs8jPjus-#;Z#!kt8)xc;$5m7Kw+TfF1Z)L7nS%1WFMqyqx3@|Ex$f1Fm}|NWo;J}nUnH=OWIWt2t`4s;($ zT8u)1AdDvby-LS`Qw8A}!sjXxV;IO-!at(KJWD7R&k@b@WY|-6SO<<{1^)X#<6~De zA*`Z_tlX+Km6HkvpTvrL0eH*ZI2qNjehl@?lu+7Kbs8|7Ml{obVQq%tjQR zVt*rp3mexMy_<+41m0(lf zp&h%3W_O5yd*W=zUUfV6vDX$#JN7d<@*&Y2Km`em;SL5XSj8ZtcF#v4T71mPI7Bpu zQHCswK0z5Z>*XV%Vlb1cW=gyrCHyTi3w(+KsnkK_XQ9Yr6_Lk7k%3?&=yrl=PKNsb z6yj6nTz?4F^&_h*)TuwQ5-$_Y&nQtIc2`iMJP-ZCV54@cn&Gbk zOIhq+LjmE__phOp>)|P&y2NXS^NPqH!E@RKHvf&}NAaWin8bgESGs!)va#+KFz=Qq zlb)C69?SE8lL_}MEA2U6LGk%urr=NmD&u&~c%UK^bpnzp15AvCgdmjje3??=BnJ8d zuZ)(-JQdGVc+J#MsncpwYRzX86hEEU%s}z-K$;mW{tutjXMwEp(3{OrkQEi6H;30G z51Y#qbOjiW(&I&5^KTX-(3rfgH0~u{AK2s)I?1;?dZR!xdqoYpZVM%zu|q3guT7OU~z-6 z7!TTe1Qaw?ot8lN+_>rL$Xa$R9_@p;Y3gWf2Ex|E&8ig3OvHi!t5D@)upWmEwn{NJ ztniAtDe7VbvDm@QQ^(aC$nbE8C8Yy|1X#$kE7e^aRB)HT+Cy~n)manm1ImgAdFd8M zB22*9nOSC??xjdDrPLadU_|m+b#fi2Xe}+HHm#m0s@OfGxF21*r<4^Bf!&~=x3tWr zgt4bmqJ*h!5!L|;+Rs{S$KGd3e|tZ+k>yQEqyNnfuLlN4vDk_|Q-{1QIa{4r2V17t zgMmeq=v4WfmGY?)`&UYw5niZM&@lTvN_;Uqen3Sgjni7_6;-LO%Be4h=N%-}cZjyK z{_GYo#t}Fi7+ii?DOieIFPKSsgD72dfqZSydQ8%8Ie4cV9{es|Q3*F__X# z={n`D8_TcQ`+|x0r92Lkcn_;5MQ=$l=D{3q1sgP(_ovVe?fZrnm!;|T zCVf^qc6-wGng7*i^y%2WV@`g@emQ-5m`(i(%sP`k#p)?Cn-eBVbdtux>%cni@ z#C-+WLKYqQ)WlZGEL0EbQ_?zkOXxUEhYi1^tTf#U<4JmuD;~5K4zUgc=X3y*rF+YY z(_zts(S%w#(+3s}^}rreDpnMMs8Ckg2^A@YV9WN{zV|-n5{IN?f74pr!ChnpnM3tk zE4F^pt&SpBapj`I>c)62af3U(6pM6mXo=g4!IJJOwilK;#0T$5Nuxsmm=>4bmqD%n z-i*SsK~}G=5L>98j4(T@HzN;BURdbrnv&O{V@^unLX1h+zS4nCM$o{8%r097c7m+^ zJZQJo^I)dll5GL%)aVB^u`vq@KG?UQQ(Csb3HQ6)>0VUr8I~^Zrg_tYG=>ucEnpBV z_SW=#2GRjE48!Nx+VxbLUJ`awRwz+=Ihh&m2BQ;7L_6%a@)lV-Wmw_Pu2eEq+W$+* z4+nf*RhaDIfWj2n9L6bcsUX>Ye>xypMNwIeiOvwNiPW&nMx;>NBf@FJD+W}hHlv)x zNCCQ1b8&i!6+$*rPo;YugV668c39F~@Cp&V8I%$kLDo*gwh!%`cj!nCA<%>oOI7w1 z5`hi6%e7}v`ek$+3hO4_neg715jCNwSUSY*85A8rHl#;SFY|gBGf@ohOW&s=OCm#i zfE!(Ej(}|qNa6E`lR@rgJN03a34HzG2*o<6!P;Fyhja#v&UEkAn;~;=@P~~hB^+ev zO0Q`$f2Yw-1vumH3|pD2(2>FFSGtXk_(Mn!H*P9UygY^C$tD{f^quB>g zFt?+uQ1qr5GUyP;Z@NCi?dcyJ$ISVIG6Lh`jPS-oY4BjuO!H!=-{UGBmSMy!V#u%= zvG*-HvP_w_OfgIEH0y1)OnsKgZno+ThOA7dPM>LV*laP&N@HbF0ThXbH@XE@J3K(J zmmO?@*_>t7TTMoj&SB0J#Vn^o)SJ-`_-dJKM$wq*uv(m%W^=?AC}sVzEf^x&|dv?xGI7Zbb{SYeMW;NGt-)Fwj1nbeMHoQCH=AJKUs!h{8SKoXQnO7YO)x026!MCO=hdZ zWOHQe&Dq8*yH&4unha)}(`L_#h`wCXAB#PB)md0JG>E>@nx!+D?O6tkJ}cYebQ+D$ zEQ876$jo-=?N+nZV9YdFL`OvIgC+g3=v&<-3gsO zSyrdfqR%u~b#{w2BJL>WhTv_bI#=nyjNtDO(F(mOL!WL*H_&OpX15wlMu*Po)H!UX ztjuhu%_^EQ?U_!K#gb{aK``2EX1zh3KTXO1M~U-+4x2qlRjbi#$+lZj3fnJkdn7K_!K0i?m}e_^ zS`ALKBO3(OTQg0zOoLst=?yx2rd6Nq%+50D40g&Pb_LC_5~@KpPF=3h-QNa#&rQ@8 zl?lWEHSDITq`?W}=2gJP?;le9O;eUbI&n~8R^HR#An$bb&oDDpIa734vNMgCl5BdD zO%Js|XUf!@b#_buHV9j&4Hk#PVN?csrBc#ZDZg_ZRhCUk9aZYY{1`+#%V@M3Z8n?3 zZp}0s44LLEv(uL4G>Jy1QJ-apk94NfZp^NVHj20zI4h{q$<&-~z#7Cr^><-Ebfq>DraJGNj9tq!bZrEZxQV9g22 zeTM}-d{V8L3$lx4SP!J-S{YS3Fmr%7+fbm&E+E>al~ue6Nc zLG-9-x^2{_p801gnA#m?&=jjsrw9sXI_z0SgTs=Ek%#juaKSX`Oqd zMp#uVU5SwqT))W-Y&7UI&AQC&Y-^_I#Ny47B|;gu>KuCTh0g56P;+1fnpq|P&0u4t z64}KM=D*-5GujMh3?k^O1}yWPx@?Rrv&m=&t`4i1Z8bts%+9o1s^q^JNJh6$4D`B- zgCk5d>5Yy|ti-Y%4y)5_wU{9#j7Cd#mJ`Iaie?cifoO`nyrJd%?r{~H2Z{#UgS55S z^j6FYSr!|RG-qMSm2EXyGwoO-i1ut|8E(Fq-t z{+UI)6U4-VOJ}xBGjK5=?M<84D4li{V!Dy_k0IWt>q8yHF zqtyYzIc%9=Qmfr$#fltq)@Z>RSDz(1m1Z(At(dmtl5R$)Qxg`m8Uz&t?9)MYua40mK_6@EQp9GrQA$@Egh;1CB4-k8ndz; zI;>T*Y!-W_PUke(Ae|tdtT1;N3^r>fROVn;ln%WiXa%hyUKm-afz^`{)SC_HVMB&K z8|zVYv(==tWo6kMI7!Vw~$x z%+5xq1qM%}Bg^8nSu9v~+OwgdiWanluJUY|7E_jJF^EbptH!yiz?(7Lm0H=-MkaK& zNtc;r5VK4$NH}2Y#H=J@!Hgp++1VDTe3_7}HnTA#MwvC@_bRD$bMq`EWTxxWbs5=qix~@dyWRvbwmp|-kc%OCd zjWAEuSWdv;0vDM63cg|%hLhEn?J(LgW0|c+yTy{N2UkNB>$1&R7y+=khUNj4ga{V( zyXmX!h*z9R!7i`7NnR0^W-W1Lz~Bm#t0~)Nu~G4B(^>R3%>3Cly%F|TBbESWhs|iC zI;$0H!pNo2-8}qvL-)~?{-g)dlqnYb0u~X1%VD=1a&TOL@rfRdpV5?+_ zWcKr$Z!gVK&@`E$*5gd01)2yJ9#}79@5ipwVeKShJ2YD~XIZcdkW~pym|U~4NXA}4 zR9xem6@HHmbFs?bAes7U(D9sJ;UhrZUf^6Ka2r*iAwLioPIwA!i(EtD_k=xi@y{t@ zQCT7UG8}34LKl1p{+!~)#T<|?@2LuC$$T5C_^;rAw*#kjZ|Q}r7U1H9UnSElJ zDtoJF+2ymOygutq6?{VlK3GBHj=-*j9W#W>o~x{Axe7d47M_t@poCho57bgshL*AX zp#<(V74R*=(!;!?WFIv(5-CBjsIF21r*_U!GN8+pGxKW4DY=$)DrD}iu@{vMe-%CM6nX~LahO)|q~c{2$v>tBZZ{b=>;}7i}5jK^euTyI8>oFoO_U739LEFK@L%6u?JnebQZ<|>U;XfE}wLz)!W zcOfY-^3u;qM^vTe#3+>>-loSsBb7^$l~bj_bI)p}mGhq09#fUvF-FOqaHbT_V{q<@ zRCaDgMY`laeRhHhB5$f%6!S*VK z^-bTrikc%~mXi?A^B!~to0pw>tpRNsutR+Z(-lOpqngQYVBgoCPoC60y9_M9X2}hhq#=jeen3i&{qm@Iad@04Tq5Y z3y;hv+;ve^;p3Gn45u8s z&H>)b*i{YQE)$tMWJ#t1l>B2pNgBI+$tSTSptl645Xy>5f)dboY>!qtcYKfbM-__y zh=F3?Vi&zDgkBv`aV`bUP90+58#tTR`Vn64O_#BU21)i!SteBgtV=c%>YZ{~lo zR?BqT5d5FQ;h{_QlhuoUMQ~ikHYD?PmkaP+n5|tl{Nilw*Q#drsocz}T~B#9r`TFJ ztkh+X(nuAE&8>SSu z&UD8kCQ~R7QUHbf&-^+%4#kIF76>alw2sKwSENhQ={OY|36jJ2 z)z?X%PM>=&spf+qr_AAU)5+K79vZoO!yzym9@F|yEjX_IUWL_PD|1g(8BCWJ9pJb@ zH%Yv7=tZur3Yg@FX673Q)#;R~%C6N{S%AI4ykdH)D&kZhN|pjsSGAB{*tM#~YE>1t zhts-;+lHeEo$1{=g{2Dny?Z^$ck0OXq>;nJ*RcaDqZTf7tR7qBbx=-57+9CQn0*pT zmZWEvY6Hi?D>Yw^J<6P`qjy0?y5xUthBolwoLSnMFC+7VCodw?TuGIrvtK6pM}l7` zt4R0M?=+sH}&$N{r&t(!Ozpsy#GuOE2tAT6OW&y|LC(_IvLQ={?d% z`;sodba`LWX;nXVtK5%aBVN9^+J#9hsiw0X1|+7x(jyxWPz2JRDxQ9~G%cpx7k^KH+t^HFs9&3F`)tbM>XiW$CNUZ2#HldgkMnq7_hw46f zW9zZjmA4n7o9S7f=k!}?Xa@a{5g&C3nmco4bVV4~!Q-7N6g@aN(;o^_J1 zsbCkHMy?|qLJCYf*Xq;DczmJijGmP{qpJANcRNdQljLS+@1|a#q@M}-6!o#ns zCN(r^d#P!WZmqCD#VS}?#yr2HrEkib9WB?Y^4}39KOI+!g!diQ`ro={`rSF&nL58%TU|zjFMZ{ zXykTF$EUYgH5HGVWtA;hL6Xh6)?&@p7q8VYYp;A<{;DAS@uimIj^g20H8&co$v3&; zGK}0{{=l+%$;S>YpO+lXD5<7+uL=+TU;+Mh(^F?$+b}(~W(ZWQt(5%NUv2s7RXld7 z8eoh<@yNDVrK~YKT23p+;8~+8DeD~WKsWlsb{X~za*d5I4`))?+jO}>TjEVcs8pd zjfOqFBNsN@AkN5lW(OWM#2Lo{ampk(dXR13FL=}tXL{*TFspQFM(VbScto>%sJY+m zRjR~*kw&eams~RejSq>0%*!YZ?fk(~AR95T)p%fA$ zTloPzVyq+;X9cR;$i(%1vr)TmnQzl?Gd^MeBAQM?RXvt0`R8E!aUS*{V^};;V)Gi(OYv)V}__bS9<1Q`2xKnCZ z?f=Rg9XPZ?8~FP03hj1PDY}@Yl;YI8i0?YktG_~Q^WGwD`50`se4r}m&#?-E8mn4GZ z2tZ`aSqoOHUPuNxgnDxZGfTexNoUV3-d}U(E>d~{yI-q6*y*o%Cl;|Q0F~?zk;{Ia zw=b#29Ek%i)U^%smwaKDQqWu1lgh8Hi(_=-i*7Eb!UTSpJ-@lW=0cKR#Y|y}d}Su^ z!oQJjS_OIelNDNN`;iqsUPxVd*CVf;{}TdfB`;yZp+N*s(cj`a2Xs5 zQ78cEux&JCV~vNCd@G_-{q=oGZ|Qn~sPc>YqOtU`hp_b2W9@umGWCn4_E zq+hI45J27AE=`I90q?LPC<#Rh0e#EIYZtz@a(oSxjNk|eH`@D8>`juEVcM>K29>Yj zRM3WAa(L<|el7Dr4Xs&QMi4_oOOz=gfO$k(ff{4jj$v@M+7#DMAB?S109~{~#h=0dK3^|_bVO-zm3pN$$c6r+C7V=aoaMNOVVu8B3yU9#o`D zflITKrOET>#IeDG>j69EM>bEfBq=cada{4VOV^V>RUsbB_aV0vva-A8ob;1}G^Fz3 zRg$H^rfW&Qxe$V5R2!Pbm5CB&k`M2_$Y_TFcgpWbzsDAZa7wD=J26}P?!MErwa=^2 z@Z2aga@qbhGr2`3NGM&BeCLwMZK&Q2%9?h-W&+ae0gUHwtg996fBAUCRCI=Gng zH?CTZjaOAT$+u*uRywh9=glHlK8_2Cj!syzcZabgGIiB3QUKD$Uk+!?XcIN2LO603 ziv5b=w^v%CY`;p!!E~!W->8i-g?gXF_M~VGPBgxS&ujOsnL19pPbIIjV)RSr{3tU3 z5+vUzC$uvTo;wl0i#a}>RtytkdpEvk7rPH5K}vxi#%ZO~6UWD~a};yh2_{N@C|kaj za8|9}RxxO3g{@+@Uwd^nY&vm!u~DBm5!>Ln!#bJ+mkI>zJFks&c;mb_(TtPe6)IUe z!az}4$h>_OZ8ETTLTX^?hZ9mSs5<|#%AH?zvnuFVsQ4O1X6i~nlK;EQZT*+W{MAgvX?aOdJ+7r1pHH_rP`59>hxI69x7L2@|@^gd7s-o_~R)xdv-v;eBn`Ul^ zHU8}Ai&8k^2NzYJt<{#FJsI6nM6HZ+j(&FM0I{$nY&Ergr}k|5j-A>V?%^fYq6+7X zuOJ;>g1I#Y{7|bW(5Su*}b^PMj9(*5j-(eL?JzSZk!ln}t z8pq3`?x?dwNt$_1d-?3KbMd>?RGj$nR;TGJM~Fgz@Nd}EX2$1Nf796@c4;EGCw8rU z#%wU?aF*2v2A z@yYbm%jb?yk6(hY(Ib~dKS>&SOjiPud`DMk=PZT`ecTcR`JTPmtusFzMI%WI=ZyHMGPYZ&OMgem4iP1QAf2` zx4_5Y7geX%-~gF2o>(8S$;ejbjNDBTK_%(X%2rF?oVlviMpe}nr+3s_g;>bro9?zU zoJ?1Y2JlH(cHph)(QT}i1~SeVd+=TDr(_3JnZSXMr>9DjF|1>#kj26EdeByZPTaQ@ zkGNf4dgWJhWGDPr!5t`}|B~o(_kQ zeBVz<9x)ybi!rzv)pLUCLy(h3Oh{dLaN&elyk~og-fg0scnd(+`sS-S8)CVE7Cu#E zP0v*HtxY?%{{2v#eo_sx8o1}?^^zS>ndX9_9aRyH04VuqtYss#K(_6%E9`#tg@b9NLzKNcgb7pP?NXfr`m)5sp)2{eWx^)+a zx^UPkaub7zl7GqJB;TqFhm(%0`mhEE>GFDI!1C}y0&qz96CXo z;#hXI3)re~aCRM+>>qP+T=IEUoBGDko0Y{^n0sABky8~8uJ!PqKCokLtO*-Cq1X^D zb%#vS*Vk%yjg{8@2Hgu2e;H;gmp!`X0?)@V7|Az(r8e-@f|c<*h0t%Uvo46-$)RM) z|1Jy$AHix6_Y%DZJ*)!3@l$l}56_QJj{8Vg4zBg^(BmWdiW(HLi>rW1KDfpQPQJP} z{)wzTB#jFFXoRc;CizFsP7W-YH9I-(3Az)8O@szQ1EeXvtsW3T(Q)=K)&_nZy;OTh zHBKI?JWi^bEDCaSBew(zl62yF(xnYJ;2O863?0o3`PjOn7$_n^O47;I$zz{8wK_R& zJ^gX&RO)c|51#Fe$QNoD$%msp2an(pP!TWI6$MmZfu@Ol&?wh0gaN>N`(%>ujRmLT zJPpu8bhx4{XlUsySqfZvT^m@6-j92@=Jl#U*MxycejKm$T|2NSu45S<@iBZqEy_BU zHiIQgKAepStovw=_OhxA`c>v%re(*KPAKy+q;ub+z z$^;ppd+=Lk5m~JY7)~~>Y3EDr!+?69ty-j0Mp^%9xkt9 z+VEfAsoi#A+Ai%_RadxgP*=o8q<#0Cd}T(S`EkN&f98w1K0*@FpWCABo5L%~E`WG1~dwNa1rAG+{5$t(eS44Y(auvb*q9s5U{NJepU^irgDaOl_!Kvcw2A{}0Jl5@ zrs9@@7+zxo20InGx_naVjAvI*ihpZ`E|{>Kjy<6^s!?7i(4O*9y)PAyX!+ds=4x}s)+2|yD#-x zpofDZ@gArdfg@cRI;9_4Y8?+U#1mCtnFC;JqD}+SJ_W8SBytsP6dIcCcl1>Vp#p{n)W0pT^-biw`F3Z~X_* zHLNH4V!@`+)j$7An{DS8Txk>cDm;7=4eMKU;S&WctrVMhc4fk0c2=2lrKy+NT$}jx?8Ob+S1tR%Jyq@eoIqmi2(W(i!u<{_O$~CHAw(W5x{=bv3W6yHd%R4_og$ z^z4VNYkmVq9F@psgy5bcT5@_O=vVXGF=|{>?qfajdl#0$u|(M;3)YDnbCyj69jkHR z`7I=5ro-;pS{5dt+iS@KO@ua!2xv|hVFh12sliEMqW!`Pa| z%g!!F=0zZ<29x}tnY4H5yV_|gB>!`B2#1UKJ)UTLA+%HFbSVI;`B#JOW~;Coev?So z3EvB#+l0|}1$yU)ASC|>=UV&U-*K+>A2S%*OMOF{o7u->~u9!*K-PB3LiuR>p!mNCA!8B4;=b_-jcec{)IE#ubnpsV(=;>vuug+|qS!a*eIJvc`Gj7J=n z=igw+gPkSXjgKrzlFnnbyc7$~7|wn6vJ!8|Iobcl39U4I%ZWH{6ze8>Z1|Q&lyRk` zO43IswEmZH_F<)JeW?1V0uG0}3kTyvIYA`>=IT-YACAYeL{MLt!6EdeAN<;HPW|YQ z->s&Li2FgJU1uFkl%&8Ot$#-OUhTK4HdMLyJ!~dX+8=qWsn%n`TAt)*>t;Tx(RB!&iFZa?%BVmj>Eqd2>8(#@ z$AA733vk@+kyD0SYa-TDkuC*rOlILP-=2tL=2Br9xq`6n$?a4F_)0*yn0+>`Nyiz- z7`GzZaij?caVuRg5TmPe4z$+G}fG(Xz4xjI>fjjn5M$xenqnH%8x6OdA-^fmWieKs7n;d5e6Vh-+!(3Pn!3Qwwe`Y z)jna~G#!URoxsP;qIpDD0>ZaPVC7i4eoy>6PzChE0yYt+kRnRMP_pD-Gyg8%#x3*j zx~8I-qxz8-8gaegOpMFJW8JqL85ZYCwziSZUEJCxZjaKOPTlBXh{&xACQ8!U-7S5S z-re0Y>a`Ur$NUhIK}|vOS0a#K(_f4zu;l=KV3Q7uK6U{djpFBCC>Wdnj ze4$!u^aJBcD!w9iLhDp9hmd^B%3Da=)|9u1vE`QEBUiCX z{TR@euY!^o?FuhN{03#3Jh>?9OaP8Z{&CpGi7|0f3V=!NA*Wi4S1x;56f)#QsHBMx1bB>A?lXyu!~dqt}l1TMlxchDHUVtNZ7 zu5xT~qvS6-BwrTF>b~$VI;Qo{!Iu=`UJd2;$hDTxi-+haj6AGWAd>XU0j=){+$(DS zfsBsI#Xi-!Zbtx={8Ns%_Fvs{ymj1zleVaUUQHOWKb2I;kFzM!$W0TIOnsz0%;JH?|QIxotTr7|H+UkrvV<|BUy!+6BFi$P5VIiC-!2#u)=Sg!(#WG-Z{D5f|vy6ZPJ!Mi`zCP zsW2J($6$gZ=2lNmVSf>u#0qI}nkBFbhW807gv$O9gz`)18amvO68Cnj8V?uk{{=R< zHQ52h8S`F{Hh$d5diH(v*#Clk+xP65AH+j?b*1*~H1P2Mf_>#aQYeBGXJcQ%w*%uo zdeS2=%4ar^Ao(wxY+HU|@|pO3Env8P9QTg)kK*o;lMx0W1&$xkp1u$`5Why0*Q+;7 zm$(>c`89qjUyGWP{` zqFdRNj@V^2$|^}){ddiH1&{a_#`GSW$8sV!oB88CmV-}M?ABiX3=Vd2uWXfX2F7*^ zt_&slXOB>!Zct5RxCKv!uBn&xv0-rC))pZb_$xfvIJ; zR{HY%?pPZ~dA;PL*oxMItI5Avi6hq49D%7yE;j6fr`X{nNH^R2w4R3~_V1lKr6 z)@qlX`SdrO7N+~{)ZizZU!v}_Q~{HG_yDQzEnFOVUBwnP*=Z@JABZfZTPO^+OdMEtTh~u&Jkehny&*oTlJfklD#+S1g*H95+i8c*?w` z9S2(r%b2Q{gRj;{v06of#4(83$qO&w5#yF0FTN`f_GLeKDlA-j<+!$H=R+v_E83Sr zK74iQ@H2S$Rpbl1e_XW-5;lfF^kBId|CYB~pIx-~ z?bdPIplO@n8Hb&-Xs#6D5Rz}=v{vv=o7O6BBVm50;CIU+5Asm56nJw>JKyzvTiV5a zTUC{Bvr^QfR9E?HuM|}BANobR?>T(Wwek@Gx*dkI1!aS*UVK~v$AdD`y^cY+N+iQ> zEq1stS>WiwKgF`p2pLnscB)u`eab+U@_G)PP zF7^S!;NX5>(;B#N#PrOXgrTW2T~-=cYD3p<@3o8NaV3Afz(BX44Ah zO>G=dRnYnORq&Kcxf%_(TcZ3k({60}#i6e(Nf#*o-`!yRi{Yjc?0~N*{^zINSXL5; zmWEaF6K%rfsW+CDFm~`=l5ke?)0S+U*0}QS*6(7aifa0yQ3}z%r&V2=*7%BwsjL|# z9@k(D<&42rPlnS?j{$l!^xlj?!z_B6-R3Y3^Ew9h&m3lWYLL$Mq{*Yhzy8G;1$1;< zz4(42-RYB{SP%Fc^&%i!R%uH<{ACZVdXlsQKNwjhhYp^2)ZK*G=wt-@1ps zpnnf_8+O~DJTFY&P9qx=ASU(3-#+@Ff0D%FbPLNTX`2oF<=s7p-b3d5kr{sv<&8Hq z8+duf*KfZ+x#RuM*Sqh&HI5z+iK7Po$#ZZ8i)<3T#Ie_2PHHxA_}=fPZVi@5(HV6A z+D78V)#v7KoZNBbzg~J@;2Milw!V87Fe5MWOMC?HKb$x_Yc<}tk&}CA`rBK!zb(IC zTEcUU#Pc72^CGg38JF-uOYTK8|e{z{sFoi$-AAI z$?wQzjUCz`n-%}#uiF<=8ZXqmIz^~Uvo>=sJ28Ki9JhUm;3|Ivk-WIDFD|yiZ>tEJpV=7I!Va!aAp%AG3?Tzi|C0_P&8%_$2|+pPTdM_z#f( zW8ru=ylx#SgU{f1)duuui{#RG2-C+<=J?``A6afcKW;K*p3`s2@0c@9ew+zva~!FgRGSm(F-QCo+*B+&h)}<4UO|doK3VACh1z+=_*8}H5QJJip?wY( z69ggm-g`AdE%<{Li-om>db#=#yjI|Z2Kfa!1s(f!=-4TzZ}$QsG?d@_^zGR*r+xR1 zyb_V$y<^ATc|AL)@SKp?Uc{GI#FWmtM5vSFgxZB*^QI&JB{b=ZN$)P$p;KnG?`jL_#+*+k{ zy)IgO_Q(+tGw7qmTQF3F+xi!}ZPr5em5q)=MSB@-i$2W|D{VA^QlYuv4nl4sa*W^Dz#54l;CxD3(1pFo7PlLZ&_^XY-I`~V( zUtRpw!(V;;HNam(juRT;&)1l%-*`o{A*9KLruE4!8=KZ9w=PdfBu$f=k!I%Rr1=lz zE^^!Kganc_mQN+xi*^1^l7C6ikQUo&)gz~qxZ7JkQJ=ILD3I0xA%WaJv5tnM46j4T z9aC#JCwE@1U7xg>M(UHc$7+?4c9%8n$X$yxZjyREK_Gv6m$W5!k4Na93k^7O@9TUD z`SVah(j;LCA?XtmrW2CEB@kV=)^q9(3Cv%dYxwE;mtxidE|3`6i;&2 z@gCCtG|JBXh|eV*xE7=%*OGL4hP=m<&g=L(q{~dc7Rj5)r<1PJ66%p|TM}!L?hvOv zzDaCKdM*S+ua{E=()$_m5lXSZc2_!$zri2uHPO?bfi9#dtm^gxj#XW9rEJyXp zyho0{=eq_iZYU8jKh%E$eXa1vUl9H){{Du)X#4;L<<7q&{U7-IC;s~35Arv`ia#6v z?D%tlZ$$h#L4iAeuHB+0J>37Hx;LUv}No+|%gUKi|i=>dzWHCRCG{1ymqYithIrW|aR|#V%YmKFkXX)#6 zc%bNMwPq7CjvP#wCXn&u^V)wV6G+#3WFl#uAdgPyrfn#bmD8M)=;H zM4oqWcfUrKBz7kYNqJ%dSwyzq(vG}N*5A^SEG9FWH6=^PRz8_5B~S~<8)Q;KB6*XH zNN7fukt>=yWI35hYLOM>5uU6hf3HhckxAq~&Q5v0eP1MggmmI>=51~WCPhlhV$e-R63IL$Z-fs#l+E zB3p&0$ou4VfhU{E?gS)%fD(*k3+SIfwvuwcoNA$ z@+-NGd_*3mxJ)b(g5()q9^@{TUxeituO zrM%8jR4IH)>-~&Aj?qVDHlhop=EunyezJz!k4|`>M5D}f(&L5`hOSf^1Spy(C~_z1 z;}m_IrjO6*1NcRs7qFb;ntegmCEiZXqBE1pmt+R{j*zd&Wnv>=lR^3-oc3KE9=oYQ`^JR>j2=SQ~SVzaz_Av?Je>mmz2`ku93>Jo$l) z)U+i(lEZa*@)J3d&<$rs#yN>4)#@d5jaK&x)g6ApSm;KsV!+oW*T~b2 z@Y*whTqlz?2@sl7>Kq}Jr6yg^)sw*30vbdM7|#DCt^(+4wuLgekDw!DBz=sckJ0q; z41J8DkFoUeEbk|Tdye0ez?0|stqDnF9RFrQZ!(_$M!1Vi;IAg!Nhb2&Ce$b8yp<=D z__9_+h91W+P3S^i;M))~ng0oT;S_!;q|Q`+I^Upjv#5zAc^a(`w1KRrk3U$#!^pwi zHIv_<8BNG6{(Gp1v-uI2FUTDJH01AGJ};5H$gd$hd5Q1aium{knm}&t6#9^t`3YnU zPhR0?k|)S~{^x7|3s z8b(g8=^_4+TI4YQHQ$(g!tcjOJ;HxLZY4+gZnep$yaWa6Gkz;+L5}gup`&lcc=i8I zMUNV_>mIM^=%gm@x)`BxXwcNGdc&j;jL)3*1m=^+$VvWfXv*XiKckj~oCec(BA@dg zX^!y!Bi^6}+ux;3w)~ubk^lQgWCl;TCFF@i{O5cEx0F2bA=<@rE65X9Nb1HEZaEFK zBHRn$jOQ%%#-?!N5qjI%j_1k|xK5VdLAZ$sopfCwJU0%ZO?D(chro|*rGM7to=51~ zzmD9)a}y9+4lSSOp2r0CHr5ge+&Gfctz80lo#d?I@w=MO8G_uKNS7v~0rx(B+YxRf z>EieaGl8FU>3uiPeMs)y1SaLU12pgo6o>sZu=xR=`-DV{u7cr2}_sf zQn*7T_Z@PPh9r{vBG2OflRL4|x3?hb6@CuC!L=QYoR1JKgK+csdzX4X0E#d3_g)2K z5pEN|pWkZ%3APa16&}BrNQ<%jnl1_4Po%}1R7CthTD-y^Y)iPy+eJhu_L*Tlp&gnNTu$xlpt<^Mo5gf8&MbUe2N zp_BZVz-ci;3&~sE2=^XB%URq~gqG;mq7lRSAsNL`<+SZYS7Yv$4*8D{CtQ;b`LpQS zzj246PCWNeS>b%p=l)YrLSIMkJ$QXSfm39_B=l~_wu$HXI^1XcPJR=44UorJ zXgETL`RB-~|2qM)yB_J$G?AObF9Qr^-B}2|lX#GC%*{mTWqu-Q%*{qu@dqw22gR zT#GuC+|Q9ZKQ(LiEYID2TLH?r^EUed(v-X7Htqd9=e%u(Fr9#WuaUa1w)KHH3u%Cw zJ3is(qO!5mfXopVoOKJ&4Vz0|Bq($6Tui_iFGrEG;nC0dL~h7jVra*4L+6rz;NIYMo1oq2N9Y< z%bd$^=LfDIn#cvnUq2%)vAAehyFLABRQp!?^9<=eho^zR=uX$7zxO`nMD;t+qo>KC z_8eDm`%1nU;ojuGW=vfokx~4Q=mV31^7Naq6KsM>09=gFJlH^ZF6k~x_a}5iX>o6n zInX#8G~uT3ACpOiIrX`P{7>XvGx*-}=fTB1ci*4)5Pf6r;k*9^Y}?=cmn9&6&fWhV zfr1{n`v6aWpW$^!80u?z-Fk-X8KN7_0{wLJ+3%NR0V#U>9>RSJ1_igw1V&G7l6$N3 z6#jGa3HZXMYfVwI>TL7}%J@Q-_g}h$tn~N!smQz(eX|nlm>>B|2wfq)4|Z!-hg-q- zUc)c{FIdIXd=yD(!gWi%%TGm2r_EUHB-P<^n;as~fDsB0@INJ{Cvb=OeZXTi8l&s^ z6o&SnOq9J_{c{uLpMn2m?AxZLJ)tT0JrTZyV1&^Bj)rzYG?vXL|5!%u;7aF{4j_@c z86`Q#ts#GVjz0rCQe^1_8fYj40cmkt`+XcO5>3A}FI1t{+`x~8xSR~w5{(SvS zWE?dge!#!SFYN}sjoXaS_P=KH+-lg%Mx?n3_jJK2Cd~i4kT86k+i9~#6Z?zM?|AOf z#&WwJOJm4w<2#)um(fel^1JvAWc6PO_xRJpo&^El=D%a(hH$U)bgp=-TLSkQ3$@vt z!fhsJ$U`XiJ(y>DPeG@B$WOy~-TOcix1XO)F7o5cf!#YW_dR#dCKUfRLT907^V|V` z0?!8aDp>v|@C_2U6Z|3c^ESYr;%$>{!Ao;=+72@O`=S!^aF$r%#4n#hd;*j_cQq!$W11? zu9Tgp5Z&S=8kk0O-RbXiqT9pxdoI!a(1ONjbP{89UUwUPJxO$uKm(_UZW4q1t1gSa zkK%Rzq`&8h?gbWqk?03 z4_II{ubWXolWuFu2;;{L*5~hp`|~m4%pqKdMZ|P_9j@st(#JTQaHC)!S;o%+QTy*_ z>CL*b_^pV)<^B3RH-_js($WIF?${hwK?7ZF8c|orgxKG8tifWdG#dF0(Ve-623{q) zPZ+wN5#0(FSU_|;S>PPez03-lN^~RYQ{Zoj?n_q53q*IG!OSMQ_gP>D(J_!YL^q9P zdXeaUX0qiaqGJ(0qFcv6=Mmi$7UR(Q*l#^uLt1)$-4pcZ7)Z>b#}Zv8jhM#kcDrd{ z4X@kEQjU`DXIW7a-@PqOSf63jyhuuf@Cw8j*$m*0UQLq)?rzSl)w>cnx|`N8tQTt}v1U{gue_el&)2prPw z8!X{C?p+#qjN^8}*0caSd>przByO{O*DR4+gr(mFeqv+7y+U-?@1V8LBDz@&sgW?N zzEOV*O8PeqEI`1n`x6Bj!|Og}R2j?bzVAk31l^zMPaRz;{Ylg@UjLh}E`2)ILFoDj*TKlE|FW_tP59{+wlg`a1FTeh-%19{;Q2^#7$dHO1Z{d;|IAW z+_{d~TvP6+RVbwy&5-@A*$#N#_G&H ziq%Fc*N3~-y_ILcYhi;fK+x$GPF2(NH@$-@l5uzj9wJ089OiJ24pr z{2c`Q(0sVb zTgcOx8o7O6kSN3oy1OFyVo^}pATB7V?5ZoS zFLsed1!X}Nq8OL`&%L**ZdF%@@VWbae59wky6Qg8J?B6FIrm(+i{=~QL?#~U-VPTs zanr(`i60n=&;Jk)ddx`N<)BXptNhFyN<5CHJ_kvE0z>psG#O!xTlZ{AAbfGl0H*61 z{AVu1;3s|yNqZ6aW<6ox6GzP+pyR}+Uob`zuV9d0Lcf66K6Dix@gA79-=jmCM$+ed(}C+5R9OniBn12;2qG}!{6op^G|OsKxZR~|Ih zL(~)R_yvCRbu;nV%Xt*c#Dh;82Y_}GN6Tpdpu}Un!vIW)Uo_1}pA%0WgBJkkka&1u zJGz(n*Hx%Y0r(vjJ;QPAGqa4 zWAOTq8bkM8I6ORZq*A%&T4VIyY0hsxV%8pN|BD~LV7&dNm$1D4a67cs%UvHZUj8Bm z;vZfz&U#^rK?`Jxm)QQ0$qe#Q6#0>|W74_JeImWQg00c%ew2%-4rf!^rH)F$b+bgv zwzYO0DipDS!s5E=D4xoDRx%kEdFpuwb-3Iq)DiQn1MNLM@^L6sJI?#((jCZi@3+UD zq80O|skFJd&vu5>uE+rz@`^a5i@bknqK02rhswh!89Z3AvX1)Y#=K$N0noBPUk z8Y$5jmT?r{<6dHv95g81X$_&+Tn(weC<%n2%FUvs+nmDKh&o6llfmXXR3WhP*x!}M z?t$V#cQ&Qe9QdQ%YfWbNs~HVlJcZ_L!=&%SfLKl@>4_Q zR>e;qBziP|@whc^@%-`A-4P^UFQj)Fy0l6$*W_MKY#LkfM(h%|xngCtuT?X&E4##; z>i5)Nk#EtvqwZNmi%QlI(nQ4_a&h$8MX_8L)7j!`pIt!SI3HFZqyRIqlMZ?y1k5|L)gh`Iaqw|coq9d)Up)vC+;a%Yh!k0 zGOcbz{jkX@gs@Oa_Kc2>*LdZ1RYZz?wuxRZciQQ1XF;hz%!{l$@ribi5dK*w6xh5^&+xpp<}RXaeUA%;>dU&sN6DolNv-ZN;?W;`b39>A-ar3 zso+lJF81*er0Qqlrhbdn)!B5OE{N(`-ij4!S&Gd$G+)0_jZ`X^q^ZRU&Vx9Ys5zB8 zkLFb5I>ZvfAs5Lt%c=Xd3V=V&N&;C7tL^R2%Y#x)&pHhTMpUf(o(Us zJFw6@&{0%f+1R|TwY}h=D5H~VUAc6qP;sqILQz8_fY3>nv ztaLEVednSbX|{5<&>DxW!F0hL72}o@_1v?oX-;``tg^K-?n2@7`RQxeGDgR#&pKpP zV%mA2UNfa~YiX8LYi!uc#dR{jTiQBU@``1Rt*16!$W;5|o~FG(-E{$-Cnn0n30v(- z#XgL!E^5}q1o3=spiRwT-D4HgZ|dq`%QK23ddF2g@^w=`D=BL))UZ8@HPc25ntM_) zt;*0j5Go22VTZgCKNat*8-3x7vw{QV48|HRQew1P88_QZ?&%V*#L??G`(c=>{{EYsaWo+ z9?E2fty+GlY{4oGPUc~Ttcg4eaxW1OMjtH>Lzsf7h$erqgJuAJ$Tsbyh1xP@qnF8G zNxLw0yJ(y0Lk;Em(w%^UoY}2JCVVXw*6U;AgO##7f^CyK?5dsJjcG(>kCL^A&McOt zLftC%3{1*?Kp4e-{k=pedeKkQcsL8S0h&>PF%}KHklq?IqS`>IF|NPDD*pKDK<;bJ z=4erKyZn7rx#srK#+cW~tPFjQw~AgsY_o5ox!&?;MV!YJ{Ta;`?m-9s?gzr$PiI9I zYgmV;atG)%Y^WAc6zjmnMo!*#@#i$CfP7_W?M3MwkUqSyei(Y{FU2%py9o`p537~& zW}12!K85ubS}a~&zyavCUCCq)3h3cjL-|52Pa9&!s0ZWk;ag$!U6dMynS*fjd!Mpz zrDUC8NYI*xsOCNSD9|iCssyko#AT3{_VW9ESyDgIkWi6kYJjgKc3=pYDWI{eH)GKZ z%!kD1Lo~&Oz8R+3UYmu}FhYw9B`)m%V_&V|hy}ovy%vlGS1T_2HaUt(>$u*QH`02>&;dbVFm2iwBSK1N;<5N7;6T_Wu$cAt_xb)p(*LvrIuv_6(bzGXEyq^j(TgeN zjgOeq@PgkrVpWs;zaBUojzg!^>@HP7iLbx{;`HUMANNmrQi%(hv}puU)fTrOwii|z}aPw;lCa%N%%ut=;3g| zDY;x~i$UYYZLRE-dUmx0O~sFd#s}yb;H4mxV;%}J@A-s--a|HGLLeMPONivkrB&c2 z`!L?NX_97QN*mw=96i!5Tzorb7c1`83FkqpSlG8x*l?JU1x>5|hYjj3A+Dg=uT?{|v z6xhyH272WU&D93vcz-JQ4s7m|GgmF`E0k;RH0bou@?-gj&bh{*KT~ZN{?rcXGik1u z>_LMsRWBNR`&R}{XCr5i*RC}}R#)o0kkx$`y5V>K-S}n6-aP^?j%)PFuc(JLVBj7W+f01=~Ty< zr5N4{y$oZ-7&+7~31QFPg#DPj0(QdR`5+qWSPU=W$lq+xPSqRaFG6;XvGiQUmGSfp6*rZiyFOS3N2jN#iA^j?^U++Z34;!?l-q!%Uvnuu&vvz?S zf}*)qcg%Kb=>hK(C?W7x*^e01cS5txSo_tLZuS;~rVgu}~O7<)!@!a6MHhB51(d z49Yh5Xw%&g=hfc>Rq_dg=3-m$K@Vm103v_VptJnNsxyIG$GOxf6N!j?d9F(Xt;h&b z9XOKikOz;TwSQ;8Py*0_Ftu3t+`or<74H&DL6Kd;EDEueR32MCB{rXeW?-y7R=eGx z^NS<55!>c7zQR_v_OJof%Ti>UCbU7Z-0TUuP&FZJ z9~BF>_T^%_5l5jd4AlVbijMNXY{Z5Z!u@8C4A|Y{qeBk7RK;I`(Th=BUp3f=3crG2>QpQBh!#SSDIue0cUvgYq#JlsVXT#))1BqQa22?tT?nA^~aJ!(StKY#iz(BOFUj3*+y%8NG?6pu8=w>{_u8>H* zKKWy~q{ur3a}0xw9A27&!hwf>V$l3Lk>)k^ zQ+(l0x_OjO0G9!&1#u~giT%s~g%4<+)>OHQCq8aaSIiXnUbi;`k+6|+(|6mhE6*0m z{s-JTxczc4B8=4&j3WdH`8nX62wFY~C?uWu;2Hl3eKU#}Arnwf;YQJoU&8&WwXW$z zNx4H7!kWLrC{zKfpBC0$^vdUR-7!$gs9$e`9U2*h2uflZh1`m2mi${I-10MUQZT0R zG1;vJwRM#)P(3N^`R*7o;7rzJcgUQBHti23%1+Af&pFbFMgQq;i z&v2I)w=dKXD~0fhe|$1d*9{(m5=|CA_c<1>z6g}c!P$@sk0B>rWv~La@)+|I@;n~F z`Tr4l9~wWws1}qyYpVY-=;L1dHNCA3Vo152Nc=nWg9h60fc64UmbFX%BbdhI)@EOk zMxuXQgb6M~H6_deM+2@f={=zisK>nqIS~C0NrryqH8=H2ljeGO57Hpq#;Z)4i3WR^ z^lGHrfG@O0`VKcBy zlV&pw&806<gCe06&vv`_(2ug*`Q`Su=LC&g3$o|k$tlX zmN=C66aeuLnbcMf8l{`h;*pej1VU7L%Se#o3?!Jx(s07$P;EzYA7Oy)hZ#C-acTezg7Yzx_N%@&;C1CPZt$g- z4$|Q3TTuuN1D}3dVPXP4@JRV!!72ZZNz;HIVeLN-)uejm(|xyL!o^(Z5M2xYPtT-q~UEGGAtLF)!M`9Sy(h1J>SN^xRE0R zxQ{QaT-v;&Ysc2UE!+B2>sBspUDMif!Rql%q05MexHYSQbT}xNygig({r4uV4x~?M zElF=NbDHej(H%-wYi%hNTUtJ4!iPStV)Z=Ro=-!(m549|L|g}nK9-YgxdUn?vX1N0 z98$SEO*$jWn1F$~CgOi!?mlPI0zf-FABG%`ZN4SH%UOe?;@r<;j<;)^ zyERd+eE|<}EF{0VwY%`KSb?e?AG_P63mO94ZS^(hl05i~bS>vgkOI%f31@`USUh6~ zx)Jn$H{zg{a}U}J2eze%ACmL z-Vc%X2Mn|(Mxr9%=+3e&Gy>;Ae$C_{r+BuofFV=t`x?h{OQijrNXyFW;5FbB)i`_e z0T_y~0+eEm%7_a?gRA}VPbTd?QM)KxEk;RXe>Ad_51I_%14|jEP!tZq*P&otD)$fu zGXU_Y+1OO4vEjn^Bj3cTF`oaXNgErzMDR30qG`+e(AsTly8;}U^6!|Gip-G= z<{}FQK_dVIBtpWg6CUrQCX*d?eTC;osg8eg0K``3sUZGq6#iVVq zks*MUG$)^74Kt=l@3U}1D1$5XrD%cW@`&iZ@{m=Wga-L8Ry49v>ZUM&5~2_cI)7}^2o`oUKoxm1cu^S{I?kO!KT0+% zDgWv4a@iHU{i#WBQA#Abjx^O08aL?{iv#ANfW^P?9eoKv=9>tbcU4&io$Sv{S`jA+ zo&fZZ0wDR5{kTcz2kH{{cs{P=Kw=U|o&XRQ=|jxAVo=0*!&b@2(9b1g7FzW`O+=Vw zucB<{0c3VRg&Fn((7GJ_63-ZAxkpOMuS|M-oYMW1LlX)^>+jVjK#xs41A&9~2t%)Q zw?JU*sLDv>uVFZgW$rgmgPf8~29bDZ8t_PY7oi$)p5Sjxx-I1Sg(kZ}d;Z_=(Ool)VJAiZm(AM`<@lF?JwmT^_4U9>tU5 z5ZQ%Q;VC#aA0MOy+H!bj$?r_gv9D8!S`<+>+qX!t?Fi_4>8$m?ggp$n(CqI`&iobY zkXJiXdujH^;^QG0XWW6Mm)<_)?3PSm_GN~D(v|&#KnCJz;atfu;S_dhqI6W`+hZn~ z2Ri;}ikKJkaE-&9svj~}S}p`hF0k|PKY-nhIZ|XUSnw;I{7|7>d8M%r*3P-B7y#+( z_z6x9sx~8Cs{<2`I|f7fM!3eNdGH3JgCQ<41mnc=Yj{Ok9Um664zfep-7=k11Qsq9wmij z{S2B?t`6B>oFQUY2nQXVA%fSg1^54oS)4c#Rb3!HWYX@ZX35|G96NOPZ` zN1Nm9WrLg=;Q(^p1G8FNO5kEG=lOZGUPpU63ttBUS)E9Xf&qQ+d}@*YO(+FZ?!VLJ zcf=};Demxd^Wot{!3wA+k(k=)r!YP@pU#Tw7uQNp7X^Xx+xfHyV;FrF)JS|vO#Q9( zugxBvPgBK4gPRu8aku$J(v*Bi0*xk9=#%~>k*IO|LRzGSAHo^v4=HLVBJ(`p#~csx8BgCALHzSwhIMs-}Hq zuL5Mar)z=@-msM%XkDZKBN_O_mw!7foP98qTT&rdwsQ7%1lIu)Oko6P*($ zFc1gfIjH$M+C-a9;I{OZK^H`8u?C3X)p&<$;3xgQiKg}J%)A5{$MCzD{y%ZJJ(OZb}iBx$*LoG)S! zrrm|IBPcay8m7oZ&pjSjB&JMC)`yqVsZn;)eQG(;s9m~t1^%-YB_1DNn7S(+-WQL&xM^O91%N6{`fsV&!Ya9%`@ ztfEB?HPGX$Xt5`Dy0l4B_2;W-t8PE^(1m6{PJkQubPJslVh>w?f-)V!oQDY==30K% z!fBCFG_3NvIODHJ_w%x;o+n!9%`s3|(eQvJVt<2L>e(3N8-#X}T9_xu`2Wxv;YzR# z`1Kl2+^W;Hcz85tL=0{uP=XxH^+X7deq}vpA4&;FX@e$DH%6oefO0F4_w(!N4Rx=? zk>4T=R1!*F(M-vXZv8rRjEFiMTOWzEu(}KbMHwlQvwv+Hoh(!OlB-)#dN=!<4efzO>Vn}r{%vwS zpWtl`D3C`t@_yL54zZ06i4Kn+)AQj6mH&d41#JllEEO2YQK~`kTG9!}4 zic!cg+0W^doJqp`&lc?YKzMEVp>|pjLhF9)|Iv0j73;SG2}dAHZgp}WYxlh_-yXa+ z|MIpjT2nXR+TvrlqW#Hl=WQTg=%Q5YfCBo~rJMh%E0i4~nSZ%89FPc`>wnWlX9vR` ziqkNR9Fl2?-q%YD0CC6tdAQvjy>v3?m&yd9f9QQ}h9UR;UYb1w zh>T(a6|q(y>!sIwVlGjcw6r_0GczeaD%V{E|DW!q#f_|BD)&M!o!0~S=!iqO97L;_ z+X(XK#LhdBM0U?kS|2yfn&X2UVj*o_+J*wdPw%83c#)3HPh&^pzazHf5O&hT708%#% z&^*pC?18(V$#|b`8KCo6VAxL&xko%Gu5jJ4hnsWf?;8N`9(8WIdpozjGN6(fA}rf| z7t)fr;f8T#uz%Y=ns(6Qz{lmyw;nif^~8Q!8J1{H-)vkl z|qd#j(+T|)Dr6G3FwGFzLn;NeFnGXYekwD zxdwOj?xCo6;eH2~LNBz6_m$~%JzY)AF(S%OlxZe{J1Xg`<=L`y-aVC{W`{be;E$FS z7{DCwMwYtTD|oDo6h}cw;<{p+UAa|7IiMa8x}kzt`FNFM-A5`qDE7Gme1P$V1%IYO zXM6J;v&(cqZqEC*P0Jx-zM+ANA8 z?GA@#q2Uadh0Ar9Blj5jVP+AL240Y{W{!X#ap-mNgrBt9N5*MUpnRl8go)suX zG@!s5r5b8UVX`FIiZX*b0{62iP_#}F6apA2EY}Vk<-$~rN$*C{AE*|mFSZ?s zz)EVsqxn#)nY97)U|WXqEsFRl2}3(&VHB$*Q9kNF>I|MlMyPI%F$}gb2)U7Q(jZ=% zLlH*W6=$b>?;wKyz>5ep=m*T}i7eD-qtpGt)IwdT%Ny&s29vLHDSVBG^{K^+awzzV zd@rb=8sLrZxl**yV9L$=}Me<5TQOAg_5R7KL zqwWbNN_h*ihIKCX+M2D{(&(+hoM8kT3Rl4xTq8W zXCwO)C4^>IAX_Z-4XW#T%E+pr6e7(HMnZBUs5ng1tcfw83e>WsR-uWGgPKCRm@xwC z6?<-juY!H;n}wLlq0l`F8NQ6U8wECL16rur4cHbB(}-|pO(Zsm73J}xf-O(BqL4Hf z=%`Z&QNt={E9UL>KjkbQxdnuKiq1gJP#BaJYPe37q><_w#ZD<5qi54vlBk=+%F_Nu z)oZ%m{J(9u-)FX0aimnZWA#1v9{}-ZSR3;U1tOzKsUERa|4f}b^8M5Zr^H5rcXFT@ zrP}Au@Eb*OLt7?f4v+_Qcy3T5k`_0XT@*-Fik;UK6X8!Em5jfzSavnqt>YM z1#|=CC8MM0Z-cBBZ;7}`Z)1-lYET+Da8@Ib&qJ;3x9$?#Ky3#VKoM~9v?xl)#{2ia z*q~IWjPG~@2Z(AFQCwDb!@sHrjZi=}q@Q@Fq>EC;Y7cqDc4#XIrHhZp%xhUnsA8<1 zCdSc2n-VvvU1KP6d!p%j8zOZfH>@im*A^v_>8y~9grn1(O^uqlw6snFdbeOaIypKZ z_%l?}rCdWB-haF!g#!s}TOF3bw$=?+sRadF)%xOrKv`OlS&=R9-d<;cf>F#Fij8fb zfV!GihJsKE+QY^w(R~tSo=Yb3?Aqkc8L;ZGV9RJW!@2Q)XxlN?cr1@)&Imu#gOos@HS|H$y@^U~hH@Y_acyX!xo@Si64A#xlJ7(BzwSVSp&8dOUOMgasgc4 zVrV(2LVcn-psi&?WPM!Fwz>sXptWRc7X_hdVeA0F8AEAkIydf1WurM(aWQt4*WQNG z-Z2!6&oQF;yt+~FTSm4*gX9T8kb2@(&q7&iwJ}IetH`SteDnx)#3Rum-&&%gI4y{-9%+_PXk7PTL 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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