Skip to content

Commit 1dfdeb8

Browse files
committed
Merge branch 'main' into feat/continuous-profiling-part1
# Conflicts: # CHANGELOG.md
2 parents c7164d7 + 6be3488 commit 1dfdeb8

File tree

14 files changed

+301
-34
lines changed

14 files changed

+301
-34
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Add native stack frame address information and debug image metadata to ANR events ([#4061](https://github.com/getsentry/sentry-java/pull/4061))
8+
- This enables symbolication for stripped native code in ANRs
79
- Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710))
810

911
To enable Continuous Profiling use the `Sentry.startProfileSession` and `Sentry.stopProfileSession` experimental APIs. Sampling rate can be set through `options.profileSessionSampleRate`, which defaults to null (disabled).

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import io.sentry.hints.AbnormalExit;
2424
import io.sentry.hints.Backfillable;
2525
import io.sentry.hints.BlockingFlushHint;
26+
import io.sentry.protocol.DebugImage;
27+
import io.sentry.protocol.DebugMeta;
2628
import io.sentry.protocol.Message;
2729
import io.sentry.protocol.SentryId;
2830
import io.sentry.protocol.SentryThread;
@@ -267,6 +269,11 @@ private void reportAsSentryEvent(
267269
event.setMessage(sentryMessage);
268270
} else if (result.type == ParseResult.Type.DUMP) {
269271
event.setThreads(result.threads);
272+
if (result.debugImages != null) {
273+
final DebugMeta debugMeta = new DebugMeta();
274+
debugMeta.setImages(result.debugImages);
275+
event.setDebugMeta(debugMeta);
276+
}
270277
}
271278
event.setLevel(SentryLevel.FATAL);
272279
event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
@@ -311,15 +318,19 @@ private void reportAsSentryEvent(
311318
final Lines lines = Lines.readLines(reader);
312319

313320
final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground);
314-
final List<SentryThread> threads = threadDumpParser.parse(lines);
321+
threadDumpParser.parse(lines);
322+
323+
final @NotNull List<SentryThread> threads = threadDumpParser.getThreads();
324+
final @NotNull List<DebugImage> debugImages = threadDumpParser.getDebugImages();
325+
315326
if (threads.isEmpty()) {
316327
// if the list is empty this means the system failed to capture a proper thread dump of
317328
// the android threads, and only contains kernel-level threads and statuses, those ANRs
318329
// are not actionable and neither they are reported by Google Play Console, so we just
319330
// fall back to not reporting them
320331
return new ParseResult(ParseResult.Type.NO_DUMP);
321332
}
322-
return new ParseResult(ParseResult.Type.DUMP, dump, threads);
333+
return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages);
323334
} catch (Throwable e) {
324335
options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
325336
return new ParseResult(ParseResult.Type.ERROR, dump);
@@ -403,24 +414,31 @@ enum Type {
403414
final Type type;
404415
final byte[] dump;
405416
final @Nullable List<SentryThread> threads;
417+
final @Nullable List<DebugImage> debugImages;
406418

407419
ParseResult(final @NotNull Type type) {
408420
this.type = type;
409421
this.dump = null;
410422
this.threads = null;
423+
this.debugImages = null;
411424
}
412425

413426
ParseResult(final @NotNull Type type, final byte[] dump) {
414427
this.type = type;
415428
this.dump = dump;
416429
this.threads = null;
430+
this.debugImages = null;
417431
}
418432

419433
ParseResult(
420-
final @NotNull Type type, final byte[] dump, final @Nullable List<SentryThread> threads) {
434+
final @NotNull Type type,
435+
final byte[] dump,
436+
final @Nullable List<SentryThread> threads,
437+
final @Nullable List<DebugImage> debugImages) {
421438
this.type = type;
422439
this.dump = dump;
423440
this.threads = threads;
441+
this.debugImages = debugImages;
424442
}
425443
}
426444
}

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

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
import io.sentry.SentryLockReason;
2323
import io.sentry.SentryOptions;
2424
import io.sentry.SentryStackTraceFactory;
25+
import io.sentry.protocol.DebugImage;
2526
import io.sentry.protocol.SentryStackFrame;
2627
import io.sentry.protocol.SentryStackTrace;
2728
import io.sentry.protocol.SentryThread;
29+
import java.math.BigInteger;
30+
import java.nio.BufferUnderflowException;
31+
import java.nio.ByteBuffer;
32+
import java.nio.ByteOrder;
2833
import java.util.ArrayList;
2934
import java.util.Collections;
3035
import java.util.HashMap;
@@ -42,12 +47,40 @@ public class ThreadDumpParser {
4247
private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE =
4348
Pattern.compile("\"(.*)\" (.*) ?sysTid=(\\d+)");
4449

50+
// For reference, see native_stack_dump.cc and tombstone_proto_to_text.cpp in Android sources
51+
// Groups
52+
// 0:entire regex
53+
// 1:index
54+
// 2:pc
55+
// 3:mapinfo
56+
// 4:filename
57+
// 5:mapoffset
58+
// 6:function
59+
// 7:fnoffset
60+
// 8:buildid
4561
private static final Pattern NATIVE_RE =
4662
Pattern.compile(
47-
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*?)\\s+\\((.*)\\+(\\d+)\\)(?: \\(.*\\))?");
48-
private static final Pattern NATIVE_NO_LOC_RE =
49-
Pattern.compile(
50-
" *(?:native: )?#\\d+ \\S+ [0-9a-fA-F]+\\s+(.*)\\s*\\(?(.*)\\)?(?: \\(.*\\))?");
63+
// " native: #12 pc 0xabcd1234"
64+
" *(?:native: )?#(\\d+) \\S+ ([0-9a-fA-F]+)"
65+
// The map info includes a filename and an optional offset into the file
66+
+ ("\\s+("
67+
// "/path/to/file.ext",
68+
+ "(.*?)"
69+
// optional " (deleted)" suffix (deleted files) needed here to bias regex
70+
// correctly
71+
+ "(?:\\s+\\(deleted\\))?"
72+
// " (offset 0xabcd1234)", if the mapping is not into the beginning of the file
73+
+ "(?:\\s+\\(offset (.*?)\\))?"
74+
+ ")")
75+
// Optional function
76+
+ ("(?:\\s+\\((?:"
77+
+ "\\?\\?\\?" // " (???) marks a missing function, so don't capture it in a group
78+
+ "|(.*?)(?:\\+(\\d+))?" // " (func+1234)", offset is
79+
// optional
80+
+ ")\\))?")
81+
// Optional " (BuildId: abcd1234abcd1234abcd1234abcd1234abcd1234)"
82+
+ "(?:\\s+\\(BuildId: (.*?)\\))?");
83+
5184
private static final Pattern JAVA_RE =
5285
Pattern.compile(" *at (?:(.+)\\.)?([^.]+)\\.([^.]+)\\((.*):([\\d-]+)\\)");
5386
private static final Pattern JNI_RE =
@@ -75,15 +108,48 @@ public class ThreadDumpParser {
75108

76109
private final @NotNull SentryStackTraceFactory stackTraceFactory;
77110

111+
private final @NotNull Map<String, DebugImage> debugImages;
112+
113+
private final @NotNull List<SentryThread> threads;
114+
78115
public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) {
79116
this.options = options;
80117
this.isBackground = isBackground;
81118
this.stackTraceFactory = new SentryStackTraceFactory(options);
119+
this.debugImages = new HashMap<>();
120+
this.threads = new ArrayList<>();
121+
}
122+
123+
@NotNull
124+
public List<DebugImage> getDebugImages() {
125+
return new ArrayList<>(debugImages.values());
82126
}
83127

84128
@NotNull
85-
public List<SentryThread> parse(final @NotNull Lines lines) {
86-
final List<SentryThread> sentryThreads = new ArrayList<>();
129+
public List<SentryThread> getThreads() {
130+
return threads;
131+
}
132+
133+
@Nullable
134+
private static String buildIdToDebugId(final @NotNull String buildId) {
135+
try {
136+
// Abuse BigInteger as a hex string parser. Extra byte needed to handle leading zeros.
137+
final ByteBuffer buf = ByteBuffer.wrap(new BigInteger("10" + buildId, 16).toByteArray());
138+
buf.get();
139+
return String.format(
140+
"%08x-%04x-%04x-%04x-%04x%08x",
141+
buf.order(ByteOrder.LITTLE_ENDIAN).getInt(),
142+
buf.getShort(),
143+
buf.getShort(),
144+
buf.order(ByteOrder.BIG_ENDIAN).getShort(),
145+
buf.getShort(),
146+
buf.getInt());
147+
} catch (NumberFormatException | BufferUnderflowException e) {
148+
return null;
149+
}
150+
}
151+
152+
public void parse(final @NotNull Lines lines) {
87153

88154
final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher("");
89155
final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher("");
@@ -92,7 +158,7 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
92158
final Line line = lines.next();
93159
if (line == null) {
94160
options.getLogger().log(SentryLevel.WARNING, "Internal error while parsing thread dump.");
95-
return sentryThreads;
161+
return;
96162
}
97163
final String text = line.text;
98164
// we only handle managed threads, as unmanaged/not attached do not have the thread id and
@@ -102,11 +168,10 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
102168

103169
final SentryThread thread = parseThread(lines);
104170
if (thread != null) {
105-
sentryThreads.add(thread);
171+
threads.add(thread);
106172
}
107173
}
108174
}
109-
return sentryThreads;
110175
}
111176

112177
private SentryThread parseThread(final @NotNull Lines lines) {
@@ -176,7 +241,6 @@ private SentryStackTrace parseStacktrace(
176241
SentryStackFrame lastJavaFrame = null;
177242

178243
final Matcher nativeRe = NATIVE_RE.matcher("");
179-
final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE.matcher("");
180244
final Matcher javaRe = JAVA_RE.matcher("");
181245
final Matcher jniRe = JNI_RE.matcher("");
182246
final Matcher lockedRe = LOCKED_RE.matcher("");
@@ -194,20 +258,7 @@ private SentryStackTrace parseStacktrace(
194258
break;
195259
}
196260
final String text = line.text;
197-
if (matches(nativeRe, text)) {
198-
final SentryStackFrame frame = new SentryStackFrame();
199-
frame.setPackage(nativeRe.group(1));
200-
frame.setFunction(nativeRe.group(2));
201-
frame.setLineno(getInteger(nativeRe, 3, null));
202-
frames.add(frame);
203-
lastJavaFrame = null;
204-
} else if (matches(nativeNoLocRe, text)) {
205-
final SentryStackFrame frame = new SentryStackFrame();
206-
frame.setPackage(nativeNoLocRe.group(1));
207-
frame.setFunction(nativeNoLocRe.group(2));
208-
frames.add(frame);
209-
lastJavaFrame = null;
210-
} else if (matches(javaRe, text)) {
261+
if (matches(javaRe, text)) {
211262
final SentryStackFrame frame = new SentryStackFrame();
212263
final String packageName = javaRe.group(1);
213264
final String className = javaRe.group(2);
@@ -219,6 +270,31 @@ private SentryStackTrace parseStacktrace(
219270
frame.setInApp(stackTraceFactory.isInApp(module));
220271
frames.add(frame);
221272
lastJavaFrame = frame;
273+
} else if (matches(nativeRe, text)) {
274+
final SentryStackFrame frame = new SentryStackFrame();
275+
frame.setPackage(nativeRe.group(3));
276+
frame.setFunction(nativeRe.group(6));
277+
frame.setLineno(getInteger(nativeRe, 7, null));
278+
frame.setInstructionAddr("0x" + nativeRe.group(2));
279+
frame.setPlatform("native");
280+
281+
final String buildId = nativeRe.group(8);
282+
final String debugId = buildId == null ? null : buildIdToDebugId(buildId);
283+
if (debugId != null) {
284+
if (!debugImages.containsKey(debugId)) {
285+
final DebugImage debugImage = new DebugImage();
286+
debugImage.setDebugId(debugId);
287+
debugImage.setType("elf");
288+
debugImage.setCodeFile(nativeRe.group(4));
289+
debugImage.setCodeId(buildId);
290+
debugImages.put(debugId, debugImage);
291+
}
292+
// The addresses in the thread dump are relative to the image
293+
frame.setAddrMode("rel:" + debugId);
294+
}
295+
296+
frames.add(frame);
297+
lastJavaFrame = null;
222298
} else if (matches(jniRe, text)) {
223299
final SentryStackFrame frame = new SentryStackFrame();
224300
final String packageName = jniRe.group(1);
@@ -227,6 +303,7 @@ private SentryStackTrace parseStacktrace(
227303
frame.setModule(module);
228304
frame.setFunction(jniRe.group(3));
229305
frame.setInApp(stackTraceFactory.isInApp(module));
306+
frame.setNative(true);
230307
frames.add(frame);
231308
lastJavaFrame = frame;
232309
} else if (matches(lockedRe, text)) {
@@ -334,8 +411,8 @@ private Long getLong(
334411

335412
@Nullable
336413
private Integer getInteger(
337-
final @NotNull Matcher matcher, final int group, final @Nullable Integer defaultValue) {
338-
final String str = matcher.group(group);
414+
final @NotNull Matcher matcher, final int groupIndex, final @Nullable Integer defaultValue) {
415+
final String str = matcher.group(groupIndex);
339416
if (str == null || str.length() == 0) {
340417
return defaultValue;
341418
} else {

sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@ class AnrV2IntegrationTest {
305305
)
306306
assertEquals("__start_thread", firstFrame.function)
307307
assertEquals(64, firstFrame.lineno)
308+
assertEquals("0x00000000000530b8", firstFrame.instructionAddr)
309+
assertEquals("native", firstFrame.platform)
310+
assertEquals("rel:741f3301-bbb0-b92c-58bd-c15282b8ec7b", firstFrame.addrMode)
311+
312+
val image = it.debugMeta?.images?.find {
313+
it.debugId == "741f3301-bbb0-b92c-58bd-c15282b8ec7b"
314+
}
315+
assertNotNull(image)
316+
assertEquals("/apex/com.android.runtime/lib64/bionic/libc.so", image.codeFile)
308317
},
309318
argThat<Hint> {
310319
val hint = HintUtils.getSentrySdkHint(this)

0 commit comments

Comments
 (0)