Skip to content

Commit e24f273

Browse files
authored
Merge c8f560d into 8e739fb
2 parents 8e739fb + c8f560d commit e24f273

15 files changed

Lines changed: 888 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- 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))
8+
- Parse ART memory and garbage collector info from ANR tombstones into ART context ([#5428](https://github.com/getsentry/sentry-java/pull/5428))
89

910
### Dependencies
1011

sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.sentry.hints.AbnormalExit;
2222
import io.sentry.hints.Backfillable;
2323
import io.sentry.hints.BlockingFlushHint;
24+
import io.sentry.protocol.ArtContext;
2425
import io.sentry.protocol.DebugImage;
2526
import io.sentry.protocol.DebugMeta;
2627
import io.sentry.protocol.Message;
@@ -173,6 +174,9 @@ public boolean shouldReportHistorical() {
173174
debugMeta.setImages(result.debugImages);
174175
event.setDebugMeta(debugMeta);
175176
}
177+
if (result.artContext != null) {
178+
event.getContexts().setArt(result.artContext);
179+
}
176180
}
177181
event.setLevel(SentryLevel.FATAL);
178182
event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
@@ -209,6 +213,7 @@ public boolean shouldReportHistorical() {
209213

210214
final @NotNull List<SentryThread> threads = threadDumpParser.getThreads();
211215
final @NotNull List<DebugImage> debugImages = threadDumpParser.getDebugImages();
216+
final @Nullable ArtContext artContext = threadDumpParser.getArtContext();
212217

213218
if (threads.isEmpty()) {
214219
// if the list is empty this means the system failed to capture a proper thread dump of
@@ -217,7 +222,7 @@ public boolean shouldReportHistorical() {
217222
// fall back to not reporting them
218223
return new ParseResult(ParseResult.Type.NO_DUMP);
219224
}
220-
return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages);
225+
return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages, artContext);
221226
} catch (Throwable e) {
222227
options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
223228
return new ParseResult(ParseResult.Type.ERROR, dump);
@@ -300,33 +305,38 @@ enum Type {
300305
}
301306

302307
final Type type;
303-
final byte[] dump;
308+
final @Nullable byte[] dump;
304309
final @Nullable List<SentryThread> threads;
305310
final @Nullable List<DebugImage> debugImages;
311+
final @Nullable ArtContext artContext;
306312

307313
ParseResult(final @NotNull Type type) {
308314
this.type = type;
309315
this.dump = null;
310316
this.threads = null;
311317
this.debugImages = null;
318+
this.artContext = null;
312319
}
313320

314321
ParseResult(final @NotNull Type type, final byte[] dump) {
315322
this.type = type;
316323
this.dump = dump;
317324
this.threads = null;
318325
this.debugImages = null;
326+
this.artContext = null;
319327
}
320328

321329
ParseResult(
322330
final @NotNull Type type,
323331
final byte[] dump,
324332
final @Nullable List<SentryThread> threads,
325-
final @Nullable List<DebugImage> debugImages) {
333+
final @Nullable List<DebugImage> debugImages,
334+
final @Nullable ArtContext artContext) {
326335
this.type = type;
327336
this.dump = dump;
328337
this.threads = threads;
329338
this.debugImages = debugImages;
339+
this.artContext = artContext;
330340
}
331341
}
332342
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package io.sentry.android.core.internal.threaddump;
2+
3+
import io.sentry.protocol.ArtContext;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
7+
/**
8+
* Parses ART runtime memory and GC metrics from ANR thread dump lines.
9+
*
10+
* @see <a href="https://android.googlesource.com/platform/art/+/master/runtime/gc/heap.cc#1282">ART
11+
* Heap::DumpGcCountRateHistogram</a>
12+
*/
13+
final class ArtContextParser {
14+
15+
private static final long KB = 1024;
16+
private static final long MB = 1024 * KB;
17+
private static final long GB = 1024 * MB;
18+
19+
private static final String FREE_MEMORY_PREFIX = "Free memory ";
20+
private static final String FREE_MEMORY_UNTIL_GC_PREFIX = "Free memory until GC ";
21+
private static final String FREE_MEMORY_UNTIL_OOME_PREFIX = "Free memory until OOME ";
22+
private static final String TOTAL_MEMORY_PREFIX = "Total memory ";
23+
private static final String MAX_MEMORY_PREFIX = "Max memory ";
24+
private static final String TOTAL_TIME_WAITING_FOR_GC_PREFIX =
25+
"Total time waiting for GC to complete: ";
26+
private static final String TOTAL_GC_COUNT_PREFIX = "Total GC count: ";
27+
private static final String TOTAL_GC_TIME_PREFIX = "Total GC time: ";
28+
private static final String TOTAL_BLOCKING_GC_COUNT_PREFIX = "Total blocking GC count: ";
29+
private static final String TOTAL_BLOCKING_GC_TIME_PREFIX = "Total blocking GC time: ";
30+
private static final String TOTAL_PRE_OOME_GC_COUNT_PREFIX = "Total pre-OOME GC count: ";
31+
32+
private @Nullable ArtContext artContext;
33+
34+
@Nullable
35+
ArtContext getArtContext() {
36+
return artContext;
37+
}
38+
39+
void parseLine(final @NotNull String text) {
40+
if (text.startsWith(FREE_MEMORY_UNTIL_OOME_PREFIX)) {
41+
getOrCreateArtContext()
42+
.setFreeMemoryUntilOome(
43+
parsePrettySize(text.substring(FREE_MEMORY_UNTIL_OOME_PREFIX.length())));
44+
} else if (text.startsWith(FREE_MEMORY_UNTIL_GC_PREFIX)) {
45+
getOrCreateArtContext()
46+
.setFreeMemoryUntilGc(
47+
parsePrettySize(text.substring(FREE_MEMORY_UNTIL_GC_PREFIX.length())));
48+
} else if (text.startsWith(FREE_MEMORY_PREFIX)) {
49+
getOrCreateArtContext()
50+
.setFreeMemory(parsePrettySize(text.substring(FREE_MEMORY_PREFIX.length())));
51+
} else if (text.startsWith(TOTAL_MEMORY_PREFIX)) {
52+
getOrCreateArtContext()
53+
.setTotalMemory(parsePrettySize(text.substring(TOTAL_MEMORY_PREFIX.length())));
54+
} else if (text.startsWith(MAX_MEMORY_PREFIX)) {
55+
getOrCreateArtContext()
56+
.setMaxMemory(parsePrettySize(text.substring(MAX_MEMORY_PREFIX.length())));
57+
} else if (text.startsWith(TOTAL_TIME_WAITING_FOR_GC_PREFIX)) {
58+
getOrCreateArtContext()
59+
.setGcWaitingTime(parseTimeMs(text.substring(TOTAL_TIME_WAITING_FOR_GC_PREFIX.length())));
60+
} else if (text.startsWith(TOTAL_GC_TIME_PREFIX)) {
61+
getOrCreateArtContext()
62+
.setGcTotalTime(parseTimeMs(text.substring(TOTAL_GC_TIME_PREFIX.length())));
63+
} else if (text.startsWith(TOTAL_GC_COUNT_PREFIX)) {
64+
getOrCreateArtContext()
65+
.setGcTotalCount(parseLongOrNull(text.substring(TOTAL_GC_COUNT_PREFIX.length())));
66+
} else if (text.startsWith(TOTAL_BLOCKING_GC_TIME_PREFIX)) {
67+
getOrCreateArtContext()
68+
.setGcBlockingTime(parseTimeMs(text.substring(TOTAL_BLOCKING_GC_TIME_PREFIX.length())));
69+
} else if (text.startsWith(TOTAL_BLOCKING_GC_COUNT_PREFIX)) {
70+
getOrCreateArtContext()
71+
.setGcBlockingCount(
72+
parseLongOrNull(text.substring(TOTAL_BLOCKING_GC_COUNT_PREFIX.length())));
73+
} else if (text.startsWith(TOTAL_PRE_OOME_GC_COUNT_PREFIX)) {
74+
getOrCreateArtContext()
75+
.setGcPreOomeCount(
76+
parseLongOrNull(text.substring(TOTAL_PRE_OOME_GC_COUNT_PREFIX.length())));
77+
}
78+
}
79+
80+
private @NotNull ArtContext getOrCreateArtContext() {
81+
if (artContext == null) {
82+
artContext = new ArtContext();
83+
}
84+
return artContext;
85+
}
86+
87+
/**
88+
* Matches Android's PrettySize output: number followed by unit with no space, e.g. "3107KB".
89+
*
90+
* <p>Counterpart to
91+
* https://cs.android.com/android/platform/superproject/+/android-latest-release:art/libartbase/base/utils.cc;l=232-251;drc=d0d3deb269b1e14de2ec2707815e38bc95de570c
92+
*/
93+
private @Nullable Long parsePrettySize(final @NotNull String sizeString) {
94+
final String trimmed = sizeString.trim();
95+
try {
96+
if (trimmed.endsWith("GB")) {
97+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * GB;
98+
} else if (trimmed.endsWith("MB")) {
99+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * MB;
100+
} else if (trimmed.endsWith("KB")) {
101+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * KB;
102+
} else if (trimmed.endsWith("B")) {
103+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 1));
104+
}
105+
} catch (NumberFormatException e) {
106+
return null;
107+
}
108+
return null;
109+
}
110+
111+
/**
112+
* Parses ART's PrettyDuration output and converts to milliseconds. Handles "s", "ms", "us", "ns"
113+
* suffixes and the bare "0" special case.
114+
*
115+
* @see <a
116+
* href="https://cs.android.com/android/platform/superproject/+/android-latest-release:art/libartbase/base/time_utils.cc;l=95-133;drc=16e1409f339b1318fe1cdce8462f089b3b0475e8">ART
117+
* PrettyDuration / FormatDuration</a>
118+
*/
119+
private static @Nullable Double parseTimeMs(final @NotNull String timeString) {
120+
final String trimmed = timeString.trim();
121+
try {
122+
if (trimmed.equals("0")) {
123+
return 0.0;
124+
}
125+
// Double.parseDouble is locale-independent (always uses '.' as decimal separator),
126+
// which matches the ART runtime output format.
127+
if (trimmed.endsWith("ms")) {
128+
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2));
129+
} else if (trimmed.endsWith("ns")) {
130+
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2)) / 1_000_000.0;
131+
} else if (trimmed.endsWith("us")) {
132+
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2)) / 1_000.0;
133+
} else if (trimmed.endsWith("s")) {
134+
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 1)) * 1_000.0;
135+
}
136+
} catch (NumberFormatException e) {
137+
return null;
138+
}
139+
return null;
140+
}
141+
142+
private static @Nullable Long parseLongOrNull(final @NotNull String value) {
143+
try {
144+
return Long.parseLong(value.trim());
145+
} catch (NumberFormatException e) {
146+
return null;
147+
}
148+
}
149+
}

sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.sentry.SentryOptions;
2424
import io.sentry.SentryStackTraceFactory;
2525
import io.sentry.android.core.internal.util.NativeEventUtils;
26+
import io.sentry.protocol.ArtContext;
2627
import io.sentry.protocol.DebugImage;
2728
import io.sentry.protocol.SentryStackFrame;
2829
import io.sentry.protocol.SentryStackTrace;
@@ -109,6 +110,8 @@ public class ThreadDumpParser {
109110

110111
private final @NotNull List<SentryThread> threads;
111112

113+
private final @NotNull ArtContextParser artContextParser = new ArtContextParser();
114+
112115
public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) {
113116
this.options = options;
114117
this.isBackground = isBackground;
@@ -127,6 +130,11 @@ public List<SentryThread> getThreads() {
127130
return threads;
128131
}
129132

133+
@Nullable
134+
public ArtContext getArtContext() {
135+
return artContextParser.getArtContext();
136+
}
137+
130138
public void parse(final @NotNull Lines lines) {
131139

132140
final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher("");
@@ -148,6 +156,8 @@ public void parse(final @NotNull Lines lines) {
148156
if (thread != null) {
149157
threads.add(thread);
150158
}
159+
} else {
160+
artContextParser.parseLine(text);
151161
}
152162
}
153163
}

0 commit comments

Comments
 (0)