Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -209,6 +213,7 @@ public boolean shouldReportHistorical() {

final @NotNull List<SentryThread> threads = threadDumpParser.getThreads();
final @NotNull List<DebugImage> 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
Expand All @@ -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);
Expand Down Expand Up @@ -300,33 +305,38 @@ enum Type {
}

final Type type;
final byte[] dump;
final @Nullable byte[] dump;
final @Nullable List<SentryThread> threads;
final @Nullable List<DebugImage> 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) {
this.type = type;
this.dump = dump;
this.threads = null;
this.debugImages = null;
this.artContext = null;
}

ParseResult(
final @NotNull Type type,
final byte[] dump,
final @Nullable List<SentryThread> threads,
final @Nullable List<DebugImage> debugImages) {
final @Nullable List<DebugImage> debugImages,
final @Nullable ArtContext artContext) {
this.type = type;
this.dump = dump;
this.threads = threads;
this.debugImages = debugImages;
this.artContext = artContext;
}
}
}
Original file line number Diff line number Diff line change
@@ -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".
*
* <p>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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -109,6 +110,8 @@ public class ThreadDumpParser {

private final @NotNull List<SentryThread> threads;

private final @NotNull ArtContextParser artContextParser = new ArtContextParser();

public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) {
this.options = options;
this.isBackground = isBackground;
Expand All @@ -127,6 +130,11 @@ public List<SentryThread> 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("");
Expand All @@ -148,6 +156,8 @@ public void parse(final @NotNull Lines lines) {
if (thread != null) {
threads.add(thread);
}
} else {
artContextParser.parseLine(text);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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)
}
}
Loading
Loading