Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 ";
Comment thread
markushi marked this conversation as resolved.
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) {
Comment thread
markushi marked this conversation as resolved.
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)
}
}
Comment thread
markushi marked this conversation as resolved.
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