Skip to content

Commit fc3a862

Browse files
authored
Merge 7edda13 into d65ecce
2 parents d65ecce + 7edda13 commit fc3a862

14 files changed

Lines changed: 762 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: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
final class ArtContextParser {
8+
9+
private static final long KB = 1024;
10+
private static final long MB = 1024 * KB;
11+
private static final long GB = 1024 * MB;
12+
13+
private static final String FREE_MEMORY_PREFIX = "Free memory ";
14+
private static final String FREE_MEMORY_UNTIL_GC_PREFIX = "Free memory until GC ";
15+
private static final String FREE_MEMORY_UNTIL_OOME_PREFIX = "Free memory until OOME ";
16+
private static final String TOTAL_MEMORY_PREFIX = "Total memory ";
17+
private static final String MAX_MEMORY_PREFIX = "Max memory ";
18+
private static final String TOTAL_TIME_WAITING_FOR_GC_PREFIX =
19+
"Total time waiting for GC to complete: ";
20+
private static final String TOTAL_GC_COUNT_PREFIX = "Total GC count: ";
21+
private static final String TOTAL_GC_TIME_PREFIX = "Total GC time: ";
22+
private static final String TOTAL_BLOCKING_GC_COUNT_PREFIX = "Total blocking GC count: ";
23+
private static final String TOTAL_BLOCKING_GC_TIME_PREFIX = "Total blocking GC time: ";
24+
private static final String TOTAL_PRE_OOME_GC_COUNT_PREFIX = "Total pre-OOME GC count: ";
25+
26+
private @Nullable ArtContext artContext;
27+
28+
@Nullable
29+
ArtContext getArtContext() {
30+
return artContext;
31+
}
32+
33+
void parseLine(final @NotNull String text) {
34+
if (text.startsWith(FREE_MEMORY_UNTIL_OOME_PREFIX)) {
35+
getOrCreateArtContext()
36+
.setFreeMemoryUntilOome(
37+
parsePrettySize(text.substring(FREE_MEMORY_UNTIL_OOME_PREFIX.length())));
38+
} else if (text.startsWith(FREE_MEMORY_UNTIL_GC_PREFIX)) {
39+
getOrCreateArtContext()
40+
.setFreeMemoryUntilGc(
41+
parsePrettySize(text.substring(FREE_MEMORY_UNTIL_GC_PREFIX.length())));
42+
} else if (text.startsWith(FREE_MEMORY_PREFIX)) {
43+
getOrCreateArtContext()
44+
.setFreeMemory(parsePrettySize(text.substring(FREE_MEMORY_PREFIX.length())));
45+
} else if (text.startsWith(TOTAL_MEMORY_PREFIX)) {
46+
getOrCreateArtContext()
47+
.setTotalMemory(parsePrettySize(text.substring(TOTAL_MEMORY_PREFIX.length())));
48+
} else if (text.startsWith(MAX_MEMORY_PREFIX)) {
49+
getOrCreateArtContext()
50+
.setMaxMemory(parsePrettySize(text.substring(MAX_MEMORY_PREFIX.length())));
51+
} else if (text.startsWith(TOTAL_TIME_WAITING_FOR_GC_PREFIX)) {
52+
getOrCreateArtContext()
53+
.setGcWaitingTime(parseTimeMs(text.substring(TOTAL_TIME_WAITING_FOR_GC_PREFIX.length())));
54+
} else if (text.startsWith(TOTAL_GC_TIME_PREFIX)) {
55+
getOrCreateArtContext()
56+
.setGcTotalTime(parseTimeMs(text.substring(TOTAL_GC_TIME_PREFIX.length())));
57+
} else if (text.startsWith(TOTAL_GC_COUNT_PREFIX)) {
58+
getOrCreateArtContext()
59+
.setGcTotalCount(parseLongOrNull(text.substring(TOTAL_GC_COUNT_PREFIX.length())));
60+
} else if (text.startsWith(TOTAL_BLOCKING_GC_TIME_PREFIX)) {
61+
getOrCreateArtContext()
62+
.setGcBlockingTime(parseTimeMs(text.substring(TOTAL_BLOCKING_GC_TIME_PREFIX.length())));
63+
} else if (text.startsWith(TOTAL_BLOCKING_GC_COUNT_PREFIX)) {
64+
getOrCreateArtContext()
65+
.setGcBlockingCount(
66+
parseLongOrNull(text.substring(TOTAL_BLOCKING_GC_COUNT_PREFIX.length())));
67+
} else if (text.startsWith(TOTAL_PRE_OOME_GC_COUNT_PREFIX)) {
68+
getOrCreateArtContext()
69+
.setGcPreOomeCount(
70+
parseLongOrNull(text.substring(TOTAL_PRE_OOME_GC_COUNT_PREFIX.length())));
71+
}
72+
}
73+
74+
private @NotNull ArtContext getOrCreateArtContext() {
75+
if (artContext == null) {
76+
artContext = new ArtContext();
77+
}
78+
return artContext;
79+
}
80+
81+
/**
82+
* Matches Android's PrettySize output: number followed by unit with no space, e.g. "3107KB".
83+
*
84+
* <p>Counterpart to
85+
* https://cs.android.com/android/platform/superproject/+/android-latest-release:art/libartbase/base/utils.cc;l=232-251;drc=d0d3deb269b1e14de2ec2707815e38bc95de570c
86+
*/
87+
private @Nullable Long parsePrettySize(final @NotNull String sizeString) {
88+
final String trimmed = sizeString.trim();
89+
try {
90+
if (trimmed.endsWith("GB")) {
91+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * GB;
92+
} else if (trimmed.endsWith("MB")) {
93+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * MB;
94+
} else if (trimmed.endsWith("KB")) {
95+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 2)) * KB;
96+
} else if (trimmed.endsWith("B")) {
97+
return Long.parseLong(trimmed.substring(0, trimmed.length() - 1));
98+
}
99+
} catch (NumberFormatException e) {
100+
return null;
101+
}
102+
return null;
103+
}
104+
105+
private static @Nullable Double parseTimeMs(final @NotNull String timeString) {
106+
final String trimmed = timeString.trim();
107+
if (trimmed.endsWith("ms")) {
108+
try {
109+
// Double.parseDouble is locale-independent (always uses '.' as decimal separator),
110+
// which matches the ART runtime output format.
111+
return Double.parseDouble(trimmed.substring(0, trimmed.length() - 2));
112+
} catch (NumberFormatException e) {
113+
return null;
114+
}
115+
}
116+
return null;
117+
}
118+
119+
private static @Nullable Long parseLongOrNull(final @NotNull String value) {
120+
try {
121+
return Long.parseLong(value.trim());
122+
} catch (NumberFormatException e) {
123+
return null;
124+
}
125+
}
126+
}

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
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package io.sentry.android.core.internal.threaddump
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertNotNull
6+
import kotlin.test.assertNull
7+
8+
class ArtContextParserTest {
9+
10+
@Test
11+
fun `parses pretty size bytes`() {
12+
val parser = ArtContextParser()
13+
parser.parseLine("Free memory 0B")
14+
assertEquals(0L, parser.artContext!!.freeMemory)
15+
16+
val parser2 = ArtContextParser()
17+
parser2.parseLine("Free memory 512B")
18+
assertEquals(512L, parser2.artContext!!.freeMemory)
19+
}
20+
21+
@Test
22+
fun `parses pretty size kilobytes`() {
23+
val parser = ArtContextParser()
24+
parser.parseLine("Free memory 3107KB")
25+
assertEquals(3107L * 1024, parser.artContext!!.freeMemory)
26+
}
27+
28+
@Test
29+
fun `parses pretty size megabytes`() {
30+
val parser = ArtContextParser()
31+
parser.parseLine("Free memory until OOME 187MB")
32+
assertEquals(187L * 1024 * 1024, parser.artContext!!.freeMemoryUntilOome)
33+
}
34+
35+
@Test
36+
fun `parses pretty size gigabytes`() {
37+
val parser = ArtContextParser()
38+
parser.parseLine("Max memory 2GB")
39+
assertEquals(2L * 1024 * 1024 * 1024, parser.artContext!!.maxMemory)
40+
}
41+
42+
@Test
43+
fun `sets null for invalid pretty size`() {
44+
val parser = ArtContextParser()
45+
parser.parseLine("Free memory 100TB")
46+
assertNull(parser.artContext!!.freeMemory)
47+
}
48+
49+
@Test
50+
fun `parses time in milliseconds`() {
51+
val parser = ArtContextParser()
52+
parser.parseLine("Total GC time: 11.807ms")
53+
assertEquals(11.807, parser.artContext!!.gcTotalTime)
54+
}
55+
56+
@Test
57+
fun `parses all memory fields`() {
58+
val parser = ArtContextParser()
59+
parser.parseLine("Free memory 3107KB")
60+
parser.parseLine("Free memory until GC 3107KB")
61+
parser.parseLine("Free memory until OOME 187MB")
62+
parser.parseLine("Total memory 7592KB")
63+
parser.parseLine("Max memory 192MB")
64+
65+
val info = parser.artContext
66+
assertNotNull(info)
67+
assertEquals(3107L * 1024, info.freeMemory)
68+
assertEquals(3107L * 1024, info.freeMemoryUntilGc)
69+
assertEquals(187L * 1024 * 1024, info.freeMemoryUntilOome)
70+
assertEquals(7592L * 1024, info.totalMemory)
71+
assertEquals(192L * 1024 * 1024, info.maxMemory)
72+
}
73+
74+
@Test
75+
fun `parses all gc fields`() {
76+
val parser = ArtContextParser()
77+
parser.parseLine("Total time waiting for GC to complete: 8.054ms")
78+
parser.parseLine("Total GC count: 1")
79+
parser.parseLine("Total GC time: 11.807ms")
80+
parser.parseLine("Total blocking GC count: 1")
81+
parser.parseLine("Total blocking GC time: 11.873ms")
82+
parser.parseLine("Total pre-OOME GC count: 0")
83+
84+
val info = parser.artContext
85+
assertNotNull(info)
86+
assertEquals(8.054, info.gcWaitingTime)
87+
assertEquals(1L, info.gcTotalCount)
88+
assertEquals(11.807, info.gcTotalTime)
89+
assertEquals(1L, info.gcBlockingCount)
90+
assertEquals(11.873, info.gcBlockingTime)
91+
assertEquals(0L, info.gcPreOomeCount)
92+
}
93+
94+
@Test
95+
fun `ignores unrelated lines`() {
96+
val parser = ArtContextParser()
97+
parser.parseLine("some random line")
98+
parser.parseLine("DALVIK THREADS (29):")
99+
parser.parseLine("")
100+
assertNull(parser.artContext)
101+
}
102+
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,28 @@ class ThreadDumpParserTest {
160160
assertEquals("ba489d4985c0cf173209da67405662f9", image.codeId)
161161
}
162162

163+
@Test
164+
fun `parses memory info from thread dump`() {
165+
val lines = Lines.readLines(File("src/test/resources/thread_dump.txt"))
166+
val parser =
167+
ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false)
168+
parser.parse(lines)
169+
170+
val artContext = parser.artContext
171+
assertNotNull(artContext)
172+
assertEquals(3107L * 1024, artContext.freeMemory)
173+
assertEquals(3107L * 1024, artContext.freeMemoryUntilGc)
174+
assertEquals(187L * 1024 * 1024, artContext.freeMemoryUntilOome)
175+
assertEquals(7592L * 1024, artContext.totalMemory)
176+
assertEquals(192L * 1024 * 1024, artContext.maxMemory)
177+
assertEquals(1L, artContext.gcTotalCount)
178+
assertEquals(11.807, artContext.gcTotalTime)
179+
assertEquals(1L, artContext.gcBlockingCount)
180+
assertEquals(11.873, artContext.gcBlockingTime)
181+
assertEquals(0L, artContext.gcPreOomeCount)
182+
assertEquals(8.054, artContext.gcWaitingTime)
183+
}
184+
163185
@Test
164186
fun `thread dump garbage`() {
165187
val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt"))
@@ -168,4 +190,13 @@ class ThreadDumpParserTest {
168190
parser.parse(lines)
169191
assertTrue(parser.threads.isEmpty())
170192
}
193+
194+
@Test
195+
fun `garbage thread dump has no memory info`() {
196+
val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt"))
197+
val parser =
198+
ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false)
199+
parser.parse(lines)
200+
assertNull(parser.artContext)
201+
}
171202
}

0 commit comments

Comments
 (0)