diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda85d8b9..91c75baa9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387)) +- Parse ART memory and garbage collector info from ANR tombstones into ART context ([#5428](https://github.com/getsentry/sentry-java/pull/5428)) ## 8.41.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index af3a942c8c..0309018ecc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -21,6 +21,7 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; +import io.sentry.protocol.ArtContext; import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.Message; @@ -173,6 +174,9 @@ public boolean shouldReportHistorical() { debugMeta.setImages(result.debugImages); event.setDebugMeta(debugMeta); } + if (result.artContext != null) { + event.getContexts().setArt(result.artContext); + } } event.setLevel(SentryLevel.FATAL); event.setTimestamp(DateUtils.getDateTime(anrTimestamp)); @@ -209,6 +213,7 @@ public boolean shouldReportHistorical() { final @NotNull List threads = threadDumpParser.getThreads(); final @NotNull List debugImages = threadDumpParser.getDebugImages(); + final @Nullable ArtContext artContext = threadDumpParser.getArtContext(); if (threads.isEmpty()) { // if the list is empty this means the system failed to capture a proper thread dump of @@ -217,7 +222,7 @@ public boolean shouldReportHistorical() { // fall back to not reporting them return new ParseResult(ParseResult.Type.NO_DUMP); } - return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages); + return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages, artContext); } catch (Throwable e) { options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e); return new ParseResult(ParseResult.Type.ERROR, dump); @@ -300,15 +305,17 @@ enum Type { } final Type type; - final byte[] dump; + final @Nullable byte[] dump; final @Nullable List threads; final @Nullable List debugImages; + final @Nullable ArtContext artContext; ParseResult(final @NotNull Type type) { this.type = type; this.dump = null; this.threads = null; this.debugImages = null; + this.artContext = null; } ParseResult(final @NotNull Type type, final byte[] dump) { @@ -316,17 +323,20 @@ enum Type { this.dump = dump; this.threads = null; this.debugImages = null; + this.artContext = null; } ParseResult( final @NotNull Type type, final byte[] dump, final @Nullable List threads, - final @Nullable List debugImages) { + final @Nullable List debugImages, + final @Nullable ArtContext artContext) { this.type = type; this.dump = dump; this.threads = threads; this.debugImages = debugImages; + this.artContext = artContext; } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ArtContextParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ArtContextParser.java new file mode 100644 index 0000000000..2ef83eef51 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ArtContextParser.java @@ -0,0 +1,126 @@ +package io.sentry.android.core.internal.threaddump; + +import io.sentry.protocol.ArtContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class ArtContextParser { + + private static final long KB = 1024; + private static final long MB = 1024 * KB; + private static final long GB = 1024 * MB; + + private static final String FREE_MEMORY_PREFIX = "Free memory "; + private static final String FREE_MEMORY_UNTIL_GC_PREFIX = "Free memory until GC "; + private static final String FREE_MEMORY_UNTIL_OOME_PREFIX = "Free memory until OOME "; + private static final String TOTAL_MEMORY_PREFIX = "Total memory "; + private static final String MAX_MEMORY_PREFIX = "Max memory "; + private static final String TOTAL_TIME_WAITING_FOR_GC_PREFIX = + "Total time waiting for GC to complete: "; + private static final String TOTAL_GC_COUNT_PREFIX = "Total GC count: "; + private static final String TOTAL_GC_TIME_PREFIX = "Total GC time: "; + private static final String TOTAL_BLOCKING_GC_COUNT_PREFIX = "Total blocking GC count: "; + private static final String TOTAL_BLOCKING_GC_TIME_PREFIX = "Total blocking GC time: "; + private static final String TOTAL_PRE_OOME_GC_COUNT_PREFIX = "Total pre-OOME GC count: "; + + private @Nullable ArtContext artContext; + + @Nullable + ArtContext getArtContext() { + return artContext; + } + + void parseLine(final @NotNull String text) { + if (text.startsWith(FREE_MEMORY_UNTIL_OOME_PREFIX)) { + getOrCreateArtContext() + .setFreeMemoryUntilOome( + parsePrettySize(text.substring(FREE_MEMORY_UNTIL_OOME_PREFIX.length()))); + } else if (text.startsWith(FREE_MEMORY_UNTIL_GC_PREFIX)) { + getOrCreateArtContext() + .setFreeMemoryUntilGc( + parsePrettySize(text.substring(FREE_MEMORY_UNTIL_GC_PREFIX.length()))); + } else if (text.startsWith(FREE_MEMORY_PREFIX)) { + getOrCreateArtContext() + .setFreeMemory(parsePrettySize(text.substring(FREE_MEMORY_PREFIX.length()))); + } else if (text.startsWith(TOTAL_MEMORY_PREFIX)) { + getOrCreateArtContext() + .setTotalMemory(parsePrettySize(text.substring(TOTAL_MEMORY_PREFIX.length()))); + } else if (text.startsWith(MAX_MEMORY_PREFIX)) { + getOrCreateArtContext() + .setMaxMemory(parsePrettySize(text.substring(MAX_MEMORY_PREFIX.length()))); + } else if (text.startsWith(TOTAL_TIME_WAITING_FOR_GC_PREFIX)) { + getOrCreateArtContext() + .setGcWaitingTime(parseTimeMs(text.substring(TOTAL_TIME_WAITING_FOR_GC_PREFIX.length()))); + } else if (text.startsWith(TOTAL_GC_TIME_PREFIX)) { + getOrCreateArtContext() + .setGcTotalTime(parseTimeMs(text.substring(TOTAL_GC_TIME_PREFIX.length()))); + } else if (text.startsWith(TOTAL_GC_COUNT_PREFIX)) { + getOrCreateArtContext() + .setGcTotalCount(parseLongOrNull(text.substring(TOTAL_GC_COUNT_PREFIX.length()))); + } else if (text.startsWith(TOTAL_BLOCKING_GC_TIME_PREFIX)) { + getOrCreateArtContext() + .setGcBlockingTime(parseTimeMs(text.substring(TOTAL_BLOCKING_GC_TIME_PREFIX.length()))); + } else if (text.startsWith(TOTAL_BLOCKING_GC_COUNT_PREFIX)) { + getOrCreateArtContext() + .setGcBlockingCount( + parseLongOrNull(text.substring(TOTAL_BLOCKING_GC_COUNT_PREFIX.length()))); + } else if (text.startsWith(TOTAL_PRE_OOME_GC_COUNT_PREFIX)) { + getOrCreateArtContext() + .setGcPreOomeCount( + parseLongOrNull(text.substring(TOTAL_PRE_OOME_GC_COUNT_PREFIX.length()))); + } + } + + private @NotNull ArtContext getOrCreateArtContext() { + if (artContext == null) { + artContext = new ArtContext(); + } + return artContext; + } + + /** + * Matches Android's PrettySize output: number followed by unit with no space, e.g. "3107KB". + * + *

Counterpart to + * https://cs.android.com/android/platform/superproject/+/android-latest-release:art/libartbase/base/utils.cc;l=232-251;drc=d0d3deb269b1e14de2ec2707815e38bc95de570c + */ + private @Nullable Long parsePrettySize(final @NotNull String sizeString) { + final String trimmed = sizeString.trim(); + try { + if (trimmed.endsWith("GB")) { + return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * GB; + } else if (trimmed.endsWith("MB")) { + return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * MB; + } else if (trimmed.endsWith("KB")) { + return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * KB; + } else if (trimmed.endsWith("B")) { + return Long.parseLong(trimmed.substring(0, trimmed.length() - 1)); + } + } catch (NumberFormatException e) { + return null; + } + return null; + } + + private static @Nullable Double parseTimeMs(final @NotNull String timeString) { + final String trimmed = timeString.trim(); + if (trimmed.endsWith("ms")) { + try { + // Double.parseDouble is locale-independent (always uses '.' as decimal separator), + // which matches the ART runtime output format. + return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2)); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + private static @Nullable Long parseLongOrNull(final @NotNull String value) { + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 5f70e39f8b..f5ce8a745c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -23,6 +23,7 @@ import io.sentry.SentryOptions; import io.sentry.SentryStackTraceFactory; import io.sentry.android.core.internal.util.NativeEventUtils; +import io.sentry.protocol.ArtContext; import io.sentry.protocol.DebugImage; import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.SentryStackTrace; @@ -109,6 +110,8 @@ public class ThreadDumpParser { private final @NotNull List threads; + private final @NotNull ArtContextParser artContextParser = new ArtContextParser(); + public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) { this.options = options; this.isBackground = isBackground; @@ -127,6 +130,11 @@ public List getThreads() { return threads; } + @Nullable + public ArtContext getArtContext() { + return artContextParser.getArtContext(); + } + public void parse(final @NotNull Lines lines) { final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); @@ -148,6 +156,8 @@ public void parse(final @NotNull Lines lines) { if (thread != null) { threads.add(thread); } + } else { + artContextParser.parseLine(text); } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ArtContextParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ArtContextParserTest.kt new file mode 100644 index 0000000000..7a17e696a3 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ArtContextParserTest.kt @@ -0,0 +1,102 @@ +package io.sentry.android.core.internal.threaddump + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ArtContextParserTest { + + @Test + fun `parses pretty size bytes`() { + val parser = ArtContextParser() + parser.parseLine("Free memory 0B") + assertEquals(0L, parser.artContext!!.freeMemory) + + val parser2 = ArtContextParser() + parser2.parseLine("Free memory 512B") + assertEquals(512L, parser2.artContext!!.freeMemory) + } + + @Test + fun `parses pretty size kilobytes`() { + val parser = ArtContextParser() + parser.parseLine("Free memory 3107KB") + assertEquals(3107L * 1024, parser.artContext!!.freeMemory) + } + + @Test + fun `parses pretty size megabytes`() { + val parser = ArtContextParser() + parser.parseLine("Free memory until OOME 187MB") + assertEquals(187L * 1024 * 1024, parser.artContext!!.freeMemoryUntilOome) + } + + @Test + fun `parses pretty size gigabytes`() { + val parser = ArtContextParser() + parser.parseLine("Max memory 2GB") + assertEquals(2L * 1024 * 1024 * 1024, parser.artContext!!.maxMemory) + } + + @Test + fun `sets null for invalid pretty size`() { + val parser = ArtContextParser() + parser.parseLine("Free memory 100TB") + assertNull(parser.artContext!!.freeMemory) + } + + @Test + fun `parses time in milliseconds`() { + val parser = ArtContextParser() + parser.parseLine("Total GC time: 11.807ms") + assertEquals(11.807, parser.artContext!!.gcTotalTime) + } + + @Test + fun `parses all memory fields`() { + val parser = ArtContextParser() + parser.parseLine("Free memory 3107KB") + parser.parseLine("Free memory until GC 3107KB") + parser.parseLine("Free memory until OOME 187MB") + parser.parseLine("Total memory 7592KB") + parser.parseLine("Max memory 192MB") + + val info = parser.artContext + assertNotNull(info) + assertEquals(3107L * 1024, info.freeMemory) + assertEquals(3107L * 1024, info.freeMemoryUntilGc) + assertEquals(187L * 1024 * 1024, info.freeMemoryUntilOome) + assertEquals(7592L * 1024, info.totalMemory) + assertEquals(192L * 1024 * 1024, info.maxMemory) + } + + @Test + fun `parses all gc fields`() { + val parser = ArtContextParser() + parser.parseLine("Total time waiting for GC to complete: 8.054ms") + parser.parseLine("Total GC count: 1") + parser.parseLine("Total GC time: 11.807ms") + parser.parseLine("Total blocking GC count: 1") + parser.parseLine("Total blocking GC time: 11.873ms") + parser.parseLine("Total pre-OOME GC count: 0") + + val info = parser.artContext + assertNotNull(info) + assertEquals(8.054, info.gcWaitingTime) + assertEquals(1L, info.gcTotalCount) + assertEquals(11.807, info.gcTotalTime) + assertEquals(1L, info.gcBlockingCount) + assertEquals(11.873, info.gcBlockingTime) + assertEquals(0L, info.gcPreOomeCount) + } + + @Test + fun `ignores unrelated lines`() { + val parser = ArtContextParser() + parser.parseLine("some random line") + parser.parseLine("DALVIK THREADS (29):") + parser.parseLine("") + assertNull(parser.artContext) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 604e2e8418..b7db35b63c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -160,6 +160,28 @@ class ThreadDumpParserTest { assertEquals("ba489d4985c0cf173209da67405662f9", image.codeId) } + @Test + fun `parses memory info from thread dump`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump.txt")) + val parser = + ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false) + parser.parse(lines) + + val artContext = parser.artContext + assertNotNull(artContext) + assertEquals(3107L * 1024, artContext.freeMemory) + assertEquals(3107L * 1024, artContext.freeMemoryUntilGc) + assertEquals(187L * 1024 * 1024, artContext.freeMemoryUntilOome) + assertEquals(7592L * 1024, artContext.totalMemory) + assertEquals(192L * 1024 * 1024, artContext.maxMemory) + assertEquals(1L, artContext.gcTotalCount) + assertEquals(11.807, artContext.gcTotalTime) + assertEquals(1L, artContext.gcBlockingCount) + assertEquals(11.873, artContext.gcBlockingTime) + assertEquals(0L, artContext.gcPreOomeCount) + assertEquals(8.054, artContext.gcWaitingTime) + } + @Test fun `thread dump garbage`() { val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) @@ -168,4 +190,13 @@ class ThreadDumpParserTest { parser.parse(lines) assertTrue(parser.threads.isEmpty()) } + + @Test + fun `garbage thread dump has no memory info`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) + val parser = + ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false) + parser.parse(lines) + assertNull(parser.artContext) + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a433abbb37..2fa836baef 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5646,6 +5646,59 @@ public final class io/sentry/protocol/App$JsonKeys { public fun ()V } +public final class io/sentry/protocol/ArtContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun equals (Ljava/lang/Object;)Z + public fun getFreeMemory ()Ljava/lang/Long; + public fun getFreeMemoryUntilGc ()Ljava/lang/Long; + public fun getFreeMemoryUntilOome ()Ljava/lang/Long; + public fun getGcBlockingCount ()Ljava/lang/Long; + public fun getGcBlockingTime ()Ljava/lang/Double; + public fun getGcPreOomeCount ()Ljava/lang/Long; + public fun getGcTotalCount ()Ljava/lang/Long; + public fun getGcTotalTime ()Ljava/lang/Double; + public fun getGcWaitingTime ()Ljava/lang/Double; + public fun getMaxMemory ()Ljava/lang/Long; + public fun getTotalMemory ()Ljava/lang/Long; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setFreeMemory (Ljava/lang/Long;)V + public fun setFreeMemoryUntilGc (Ljava/lang/Long;)V + public fun setFreeMemoryUntilOome (Ljava/lang/Long;)V + public fun setGcBlockingCount (Ljava/lang/Long;)V + public fun setGcBlockingTime (Ljava/lang/Double;)V + public fun setGcPreOomeCount (Ljava/lang/Long;)V + public fun setGcTotalCount (Ljava/lang/Long;)V + public fun setGcTotalTime (Ljava/lang/Double;)V + public fun setGcWaitingTime (Ljava/lang/Double;)V + public fun setMaxMemory (Ljava/lang/Long;)V + public fun setTotalMemory (Ljava/lang/Long;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/protocol/ArtContext$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/protocol/ArtContext; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/protocol/ArtContext$JsonKeys { + public static final field FREE_MEMORY Ljava/lang/String; + public static final field FREE_MEMORY_UNTIL_GC Ljava/lang/String; + public static final field FREE_MEMORY_UNTIL_OOME Ljava/lang/String; + public static final field GC_BLOCKING_COUNT Ljava/lang/String; + public static final field GC_BLOCKING_TIME Ljava/lang/String; + public static final field GC_PRE_OOME_COUNT Ljava/lang/String; + public static final field GC_TOTAL_COUNT Ljava/lang/String; + public static final field GC_TOTAL_TIME Ljava/lang/String; + public static final field GC_WAITING_TIME Ljava/lang/String; + public static final field MAX_MEMORY Ljava/lang/String; + public static final field TOTAL_MEMORY Ljava/lang/String; + public fun ()V +} + public final class io/sentry/protocol/Browser : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V @@ -5682,6 +5735,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun equals (Ljava/lang/Object;)Z public fun get (Ljava/lang/Object;)Ljava/lang/Object; public fun getApp ()Lio/sentry/protocol/App; + public fun getArt ()Lio/sentry/protocol/ArtContext; public fun getBrowser ()Lio/sentry/protocol/Browser; public fun getDevice ()Lio/sentry/protocol/Device; public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; @@ -5704,6 +5758,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun set (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; public fun setApp (Lio/sentry/protocol/App;)V + public fun setArt (Lio/sentry/protocol/ArtContext;)V public fun setBrowser (Lio/sentry/protocol/Browser;)V public fun setDevice (Lio/sentry/protocol/Device;)V public fun setFeatureFlags (Lio/sentry/protocol/FeatureFlags;)V diff --git a/sentry/src/main/java/io/sentry/protocol/ArtContext.java b/sentry/src/main/java/io/sentry/protocol/ArtContext.java new file mode 100644 index 0000000000..eeffb95b27 --- /dev/null +++ b/sentry/src/main/java/io/sentry/protocol/ArtContext.java @@ -0,0 +1,331 @@ +package io.sentry.protocol; + +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.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Context containing ART (Android Runtime) specific information. This is only relevant for Android + * and may be null on other platforms. + */ +public final class ArtContext implements JsonUnknown, JsonSerializable { + public static final String TYPE = "art"; + + private @Nullable Long gcTotalCount; + private @Nullable Double gcTotalTime; + private @Nullable Long gcBlockingCount; + private @Nullable Double gcBlockingTime; + private @Nullable Long gcPreOomeCount; + private @Nullable Double gcWaitingTime; + private @Nullable Long freeMemory; + private @Nullable Long freeMemoryUntilGc; + private @Nullable Long freeMemoryUntilOome; + private @Nullable Long totalMemory; + private @Nullable Long maxMemory; + + @SuppressWarnings("unused") + private @Nullable Map unknown; + + public ArtContext() {} + + ArtContext(final @NotNull ArtContext other) { + this.gcTotalCount = other.gcTotalCount; + this.gcTotalTime = other.gcTotalTime; + this.gcBlockingCount = other.gcBlockingCount; + this.gcBlockingTime = other.gcBlockingTime; + this.gcPreOomeCount = other.gcPreOomeCount; + this.gcWaitingTime = other.gcWaitingTime; + this.freeMemory = other.freeMemory; + this.freeMemoryUntilGc = other.freeMemoryUntilGc; + this.freeMemoryUntilOome = other.freeMemoryUntilOome; + this.totalMemory = other.totalMemory; + this.maxMemory = other.maxMemory; + this.unknown = CollectionUtils.newConcurrentHashMap(other.unknown); + } + + /** Total number of GC collections since process start. */ + public @Nullable Long getGcTotalCount() { + return gcTotalCount; + } + + /** Total number of GC collections since process start. */ + public void setGcTotalCount(final @Nullable Long gcTotalCount) { + this.gcTotalCount = gcTotalCount; + } + + /** Total time spent in GC since process start, in milliseconds. */ + public @Nullable Double getGcTotalTime() { + return gcTotalTime; + } + + /** Total time spent in GC since process start, in milliseconds. */ + public void setGcTotalTime(final @Nullable Double gcTotalTime) { + this.gcTotalTime = gcTotalTime; + } + + /** Total number of blocking (stop-the-world) GC collections since process start. */ + public @Nullable Long getGcBlockingCount() { + return gcBlockingCount; + } + + /** Total number of blocking (stop-the-world) GC collections since process start. */ + public void setGcBlockingCount(final @Nullable Long gcBlockingCount) { + this.gcBlockingCount = gcBlockingCount; + } + + /** Total time spent in blocking (stop-the-world) GC since process start, in milliseconds. */ + public @Nullable Double getGcBlockingTime() { + return gcBlockingTime; + } + + /** Total time spent in blocking (stop-the-world) GC since process start, in milliseconds. */ + public void setGcBlockingTime(final @Nullable Double gcBlockingTime) { + this.gcBlockingTime = gcBlockingTime; + } + + /** Total number of GC collections triggered to prevent an OutOfMemoryError. */ + public @Nullable Long getGcPreOomeCount() { + return gcPreOomeCount; + } + + /** Total number of GC collections triggered to prevent an OutOfMemoryError. */ + public void setGcPreOomeCount(final @Nullable Long gcPreOomeCount) { + this.gcPreOomeCount = gcPreOomeCount; + } + + /** Total time threads spent waiting for GC to complete, in milliseconds. */ + public @Nullable Double getGcWaitingTime() { + return gcWaitingTime; + } + + /** Total time threads spent waiting for GC to complete, in milliseconds. */ + public void setGcWaitingTime(final @Nullable Double gcWaitingTime) { + this.gcWaitingTime = gcWaitingTime; + } + + /** Free memory available in the managed heap, in bytes. */ + public @Nullable Long getFreeMemory() { + return freeMemory; + } + + /** Free memory available in the managed heap, in bytes. */ + public void setFreeMemory(final @Nullable Long freeMemory) { + this.freeMemory = freeMemory; + } + + /** Free memory available until the next GC is triggered, in bytes. */ + public @Nullable Long getFreeMemoryUntilGc() { + return freeMemoryUntilGc; + } + + /** Free memory available until the next GC is triggered, in bytes. */ + public void setFreeMemoryUntilGc(final @Nullable Long freeMemoryUntilGc) { + this.freeMemoryUntilGc = freeMemoryUntilGc; + } + + /** Free memory available until an OutOfMemoryError is thrown, in bytes. */ + public @Nullable Long getFreeMemoryUntilOome() { + return freeMemoryUntilOome; + } + + /** Free memory available until an OutOfMemoryError is thrown, in bytes. */ + public void setFreeMemoryUntilOome(final @Nullable Long freeMemoryUntilOome) { + this.freeMemoryUntilOome = freeMemoryUntilOome; + } + + /** Total memory currently allocated for the managed heap, in bytes. */ + public @Nullable Long getTotalMemory() { + return totalMemory; + } + + /** Total memory currently allocated for the managed heap, in bytes. */ + public void setTotalMemory(final @Nullable Long totalMemory) { + this.totalMemory = totalMemory; + } + + /** Maximum memory the managed heap is allowed to grow to, in bytes. */ + public @Nullable Long getMaxMemory() { + return maxMemory; + } + + /** Maximum memory the managed heap is allowed to grow to, in bytes. */ + public void setMaxMemory(final @Nullable Long maxMemory) { + this.maxMemory = maxMemory; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ArtContext that = (ArtContext) o; + return Objects.equals(gcTotalCount, that.gcTotalCount) + && Objects.equals(gcTotalTime, that.gcTotalTime) + && Objects.equals(gcBlockingCount, that.gcBlockingCount) + && Objects.equals(gcBlockingTime, that.gcBlockingTime) + && Objects.equals(gcPreOomeCount, that.gcPreOomeCount) + && Objects.equals(gcWaitingTime, that.gcWaitingTime) + && Objects.equals(freeMemory, that.freeMemory) + && Objects.equals(freeMemoryUntilGc, that.freeMemoryUntilGc) + && Objects.equals(freeMemoryUntilOome, that.freeMemoryUntilOome) + && Objects.equals(totalMemory, that.totalMemory) + && Objects.equals(maxMemory, that.maxMemory); + } + + @Override + public int hashCode() { + return Objects.hash( + gcTotalCount, + gcTotalTime, + gcBlockingCount, + gcBlockingTime, + gcPreOomeCount, + gcWaitingTime, + freeMemory, + freeMemoryUntilGc, + freeMemoryUntilOome, + totalMemory, + maxMemory); + } + + // region JsonSerializable + + public static final class JsonKeys { + public static final String GC_TOTAL_COUNT = "gc_total_count"; + public static final String GC_TOTAL_TIME = "gc_total_time"; + public static final String GC_BLOCKING_COUNT = "gc_blocking_count"; + public static final String GC_BLOCKING_TIME = "gc_blocking_time"; + public static final String GC_PRE_OOME_COUNT = "gc_pre_oome_count"; + public static final String GC_WAITING_TIME = "gc_waiting_time"; + public static final String FREE_MEMORY = "free_memory"; + public static final String FREE_MEMORY_UNTIL_GC = "free_memory_until_gc"; + public static final String FREE_MEMORY_UNTIL_OOME = "free_memory_until_oome"; + public static final String TOTAL_MEMORY = "total_memory"; + public static final String MAX_MEMORY = "max_memory"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + if (gcTotalCount != null) { + writer.name(JsonKeys.GC_TOTAL_COUNT).value(gcTotalCount); + } + if (gcTotalTime != null) { + writer.name(JsonKeys.GC_TOTAL_TIME).value(gcTotalTime); + } + if (gcBlockingCount != null) { + writer.name(JsonKeys.GC_BLOCKING_COUNT).value(gcBlockingCount); + } + if (gcBlockingTime != null) { + writer.name(JsonKeys.GC_BLOCKING_TIME).value(gcBlockingTime); + } + if (gcPreOomeCount != null) { + writer.name(JsonKeys.GC_PRE_OOME_COUNT).value(gcPreOomeCount); + } + if (gcWaitingTime != null) { + writer.name(JsonKeys.GC_WAITING_TIME).value(gcWaitingTime); + } + if (freeMemory != null) { + writer.name(JsonKeys.FREE_MEMORY).value(freeMemory); + } + if (freeMemoryUntilGc != null) { + writer.name(JsonKeys.FREE_MEMORY_UNTIL_GC).value(freeMemoryUntilGc); + } + if (freeMemoryUntilOome != null) { + writer.name(JsonKeys.FREE_MEMORY_UNTIL_OOME).value(freeMemoryUntilOome); + } + if (totalMemory != null) { + writer.name(JsonKeys.TOTAL_MEMORY).value(totalMemory); + } + if (maxMemory != null) { + writer.name(JsonKeys.MAX_MEMORY).value(maxMemory); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key); + writer.value(logger, value); + } + } + writer.endObject(); + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull ArtContext deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + ArtContext artContext = new ArtContext(); + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.GC_TOTAL_COUNT: + artContext.gcTotalCount = reader.nextLongOrNull(); + break; + case JsonKeys.GC_TOTAL_TIME: + artContext.gcTotalTime = reader.nextDoubleOrNull(); + break; + case JsonKeys.GC_BLOCKING_COUNT: + artContext.gcBlockingCount = reader.nextLongOrNull(); + break; + case JsonKeys.GC_BLOCKING_TIME: + artContext.gcBlockingTime = reader.nextDoubleOrNull(); + break; + case JsonKeys.GC_PRE_OOME_COUNT: + artContext.gcPreOomeCount = reader.nextLongOrNull(); + break; + case JsonKeys.GC_WAITING_TIME: + artContext.gcWaitingTime = reader.nextDoubleOrNull(); + break; + case JsonKeys.FREE_MEMORY: + artContext.freeMemory = reader.nextLongOrNull(); + break; + case JsonKeys.FREE_MEMORY_UNTIL_GC: + artContext.freeMemoryUntilGc = reader.nextLongOrNull(); + break; + case JsonKeys.FREE_MEMORY_UNTIL_OOME: + artContext.freeMemoryUntilOome = reader.nextLongOrNull(); + break; + case JsonKeys.TOTAL_MEMORY: + artContext.totalMemory = reader.nextLongOrNull(); + break; + case JsonKeys.MAX_MEMORY: + artContext.maxMemory = reader.nextLongOrNull(); + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + artContext.setUnknown(unknown); + reader.endObject(); + return artContext; + } + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index 553f4ddbd3..fd1e9b83eb 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -64,6 +64,8 @@ public Contexts(final @NotNull Contexts contexts) { this.setResponse(new Response((Response) value)); } else if (Spring.TYPE.equals(entry.getKey()) && value instanceof Spring) { this.setSpring(new Spring((Spring) value)); + } else if (ArtContext.TYPE.equals(entry.getKey()) && value instanceof ArtContext) { + this.setArt(new ArtContext((ArtContext) value)); } else { this.put(entry.getKey(), value); } @@ -181,6 +183,14 @@ public void setSpring(final @NotNull Spring spring) { this.put(Spring.TYPE, spring); } + public @Nullable ArtContext getArt() { + return toContextType(ArtContext.TYPE, ArtContext.class); + } + + public void setArt(final @NotNull ArtContext art) { + this.put(ArtContext.TYPE, art); + } + public @Nullable FeatureFlags getFeatureFlags() { return toContextType(FeatureFlags.TYPE, FeatureFlags.class); } @@ -347,6 +357,9 @@ public static final class Deserializer implements JsonDeserializer { case Spring.TYPE: contexts.setSpring(new Spring.Deserializer().deserialize(reader, logger)); break; + case ArtContext.TYPE: + contexts.setArt(new ArtContext.Deserializer().deserialize(reader, logger)); + break; case FeatureFlags.TYPE: contexts.setFeatureFlags(new FeatureFlags.Deserializer().deserialize(reader, logger)); break; diff --git a/sentry/src/test/java/io/sentry/protocol/ArtContextSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/ArtContextSerializationTest.kt new file mode 100644 index 0000000000..57d6411baa --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/ArtContextSerializationTest.kt @@ -0,0 +1,51 @@ +package io.sentry.protocol + +import io.sentry.ILogger +import kotlin.test.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock + +class ArtContextSerializationTest { + class Fixture { + val logger = mock() + + fun getSut() = + ArtContext().apply { + gcTotalCount = 1L + gcTotalTime = 11.807 + gcBlockingCount = 1L + gcBlockingTime = 11.873 + gcPreOomeCount = 0L + gcWaitingTime = 8.054 + freeMemory = 3181568L + freeMemoryUntilGc = 3181568L + freeMemoryUntilOome = 196083712L + totalMemory = 7774208L + maxMemory = 201326592L + } + } + + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = SerializationUtils.sanitizedFile("json/art_context.json") + val actual = SerializationUtils.serializeToString(fixture.getSut(), fixture.logger) + + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = SerializationUtils.sanitizedFile("json/art_context.json") + val actual = + SerializationUtils.deserializeJson( + expectedJson, + ArtContext.Deserializer(), + fixture.logger, + ) + val actualJson = SerializationUtils.serializeToString(actual, fixture.logger) + + assertEquals(expectedJson, actualJson) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt index d7fd3cf9f7..33db7a2e29 100644 --- a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt @@ -22,6 +22,7 @@ class CombinedContextsViewSerializationTest { val combined = CombinedContextsView(global, isolation, current, ScopeType.ISOLATION) current.setApp(AppSerializationTest.Fixture().getSut()) + current.setArt(ArtContextSerializationTest.Fixture().getSut()) current.setBrowser(BrowserSerializationTest.Fixture().getSut()) current.setFeedback(FeedbackTest.Fixture().getSut()) current.setTrace(SpanContextSerializationTest.Fixture().getSut()) diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt index 1a5e252a76..8e17de9c68 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt @@ -25,6 +25,7 @@ class ContextsSerializationTest { setResponse(ResponseSerializationTest.Fixture().getSut()) setTrace(SpanContextSerializationTest.Fixture().getSut()) setSpring(SpringSerializationTest.Fixture().getSut()) + setArt(ArtContextSerializationTest.Fixture().getSut()) setFeatureFlags(FeatureFlagsSerializationTest.Fixture().getSut()) } } diff --git a/sentry/src/test/resources/json/art_context.json b/sentry/src/test/resources/json/art_context.json new file mode 100644 index 0000000000..0e336a0344 --- /dev/null +++ b/sentry/src/test/resources/json/art_context.json @@ -0,0 +1,13 @@ +{ + "gc_total_count": 1, + "gc_total_time": 11.807, + "gc_blocking_count": 1, + "gc_blocking_time": 11.873, + "gc_pre_oome_count": 0, + "gc_waiting_time": 8.054, + "free_memory": 3181568, + "free_memory_until_gc": 3181568, + "free_memory_until_oome": 196083712, + "total_memory": 7774208, + "max_memory": 201326592 +} diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index 7f4c0c16bc..886a260e23 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -17,6 +17,20 @@ "view_names": ["MainActivity", "SidebarActivity"], "start_type": "cold" }, + "art": + { + "gc_total_count": 1, + "gc_total_time": 11.807, + "gc_blocking_count": 1, + "gc_blocking_time": 11.873, + "gc_pre_oome_count": 0, + "gc_waiting_time": 8.054, + "free_memory": 3181568, + "free_memory_until_gc": 3181568, + "free_memory_until_oome": 196083712, + "total_memory": 7774208, + "max_memory": 201326592 + }, "browser": { "name": "e1c723db-7408-4043-baa7-f4e96234e5dc",