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,149 @@
package io.sentry.android.core.internal.threaddump;

import io.sentry.protocol.ArtContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Parses ART runtime memory and GC metrics from ANR thread dump lines.
*
* @see <a href="https://android.googlesource.com/platform/art/+/master/runtime/gc/heap.cc#1282">ART
* Heap::DumpGcCountRateHistogram</a>
*/
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 ";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a sense for how frequently these strings change, and how stable they are across OEMs? Just wondering how much we need to monitor the presence of unparseable / null values here vs our ability to set it and forget it.

(Doesn't have to hold up this PR, regardless.)

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a source URL for the substrings we parse against that we could link to in a Javadoc? (eg, here?) That'd let folks know where to look if things change + where they can double-check semantics.

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;
}

/**
* Parses ART's PrettyDuration output and converts to milliseconds. Handles "s", "ms", "us", "ns"
* suffixes and the bare "0" special case.
*
* @see <a
* href="https://cs.android.com/android/platform/superproject/+/android-latest-release:art/libartbase/base/time_utils.cc;l=95-133;drc=16e1409f339b1318fe1cdce8462f089b3b0475e8">ART
* PrettyDuration / FormatDuration</a>
*/
private static @Nullable Double parseTimeMs(final @NotNull String timeString) {
final String trimmed = timeString.trim();
try {
if (trimmed.equals("0")) {
return 0.0;
}
// Double.parseDouble is locale-independent (always uses '.' as decimal separator),
// which matches the ART runtime output format.
if (trimmed.endsWith("ms")) {
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2));
} else if (trimmed.endsWith("ns")) {
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2)) / 1_000_000.0;
} else if (trimmed.endsWith("us")) {
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2)) / 1_000.0;
} else if (trimmed.endsWith("s")) {
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 1)) * 1_000.0;
}
} 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
Loading
Loading