Skip to content

Commit b286ad5

Browse files
committed
Profile main thread when ANR and report ANR profiles to sentry
1 parent 7ca207f commit b286ad5

24 files changed

+1636
-34
lines changed

sentry-android-core/api/sentry-android-core.api

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
326326
public fun isCollectAdditionalContext ()Z
327327
public fun isEnableActivityLifecycleBreadcrumbs ()Z
328328
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
329+
public fun isEnableAnrProfiling ()Z
329330
public fun isEnableAppComponentBreadcrumbs ()Z
330331
public fun isEnableAppLifecycleBreadcrumbs ()Z
331332
public fun isEnableAutoActivityLifecycleTracing ()Z
@@ -351,6 +352,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
351352
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
352353
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
353354
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
355+
public fun setEnableAnrProfiling (Z)V
354356
public fun setEnableAppComponentBreadcrumbs (Z)V
355357
public fun setEnableAppLifecycleBreadcrumbs (Z)V
356358
public fun setEnableAutoActivityLifecycleTracing (Z)V
@@ -480,6 +482,62 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
480482
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
481483
}
482484

485+
public class io/sentry/android/core/anr/AggregatedStackTrace {
486+
public fun <init> ([Ljava/lang/StackTraceElement;IIJI)V
487+
public fun add (J)V
488+
public fun getStack ()[Ljava/lang/StackTraceElement;
489+
}
490+
491+
public class io/sentry/android/core/anr/AnrCulpritIdentifier {
492+
public fun <init> ()V
493+
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
494+
}
495+
496+
public class io/sentry/android/core/anr/AnrException : java/lang/Exception {
497+
public fun <init> ()V
498+
public fun <init> (Ljava/lang/String;)V
499+
}
500+
501+
public class io/sentry/android/core/anr/AnrProfile {
502+
public final field endtimeMs J
503+
public final field stacks Ljava/util/List;
504+
public final field startTimeMs J
505+
public fun <init> (Ljava/util/List;)V
506+
}
507+
508+
public class io/sentry/android/core/anr/AnrProfileManager {
509+
public fun <init> (Lio/sentry/SentryOptions;)V
510+
public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
511+
public fun clear ()V
512+
public fun load ()Lio/sentry/android/core/anr/AnrProfile;
513+
}
514+
515+
public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
516+
public static final field POLLING_INTERVAL_MS J
517+
public static final field THRESHOLD_ANR_MS J
518+
public fun <init> ()V
519+
public fun close ()V
520+
public fun onBackground ()V
521+
public fun onForeground ()V
522+
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
523+
public fun run ()V
524+
}
525+
526+
public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
527+
public final field stack [Ljava/lang/StackTraceElement;
528+
public final field timestampMs J
529+
public fun <init> (J[Ljava/lang/StackTraceElement;)V
530+
public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
531+
public synthetic fun compareTo (Ljava/lang/Object;)I
532+
public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
533+
public fun serialize (Ljava/io/DataOutputStream;)V
534+
}
535+
536+
public final class io/sentry/android/core/anr/StackTraceConverter {
537+
public fun <init> ()V
538+
public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
539+
}
540+
483541
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
484542
public static final field LAST_ANR_REPORT Ljava/lang/String;
485543
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.sentry.SendFireAndForgetOutboxSender;
2626
import io.sentry.SentryLevel;
2727
import io.sentry.SentryOpenTelemetryMode;
28+
import io.sentry.android.core.anr.AnrProfilingIntegration;
2829
import io.sentry.android.core.cache.AndroidEnvelopeCache;
2930
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
3031
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
@@ -391,6 +392,10 @@ static void installDefaultIntegrations(
391392
// it to set the replayId in case of an ANR
392393
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
393394

395+
if (options.isEnableAnrProfiling()) {
396+
options.addIntegration(new AnrProfilingIntegration());
397+
}
398+
394399
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
395400
if (context instanceof Application) {
396401
options.addIntegration(

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@
1212
import io.sentry.ILogger;
1313
import io.sentry.IScopes;
1414
import io.sentry.Integration;
15+
import io.sentry.ProfileChunk;
16+
import io.sentry.ProfileContext;
1517
import io.sentry.SentryEvent;
18+
import io.sentry.SentryExceptionFactory;
1619
import io.sentry.SentryLevel;
1720
import io.sentry.SentryOptions;
21+
import io.sentry.SentryStackTraceFactory;
22+
import io.sentry.android.core.anr.AggregatedStackTrace;
23+
import io.sentry.android.core.anr.AnrCulpritIdentifier;
24+
import io.sentry.android.core.anr.AnrException;
25+
import io.sentry.android.core.anr.AnrProfile;
26+
import io.sentry.android.core.anr.AnrProfileManager;
27+
import io.sentry.android.core.anr.StackTraceConverter;
1828
import io.sentry.android.core.cache.AndroidEnvelopeCache;
1929
import io.sentry.android.core.internal.threaddump.Lines;
2030
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
@@ -28,6 +38,7 @@
2838
import io.sentry.protocol.Message;
2939
import io.sentry.protocol.SentryId;
3040
import io.sentry.protocol.SentryThread;
41+
import io.sentry.protocol.profiling.SentryProfile;
3142
import io.sentry.transport.CurrentDateProvider;
3243
import io.sentry.transport.ICurrentDateProvider;
3344
import io.sentry.util.HintUtils;
@@ -41,6 +52,7 @@
4152
import java.io.InputStreamReader;
4253
import java.util.ArrayList;
4354
import java.util.Collections;
55+
import java.util.HashMap;
4456
import java.util.List;
4557
import java.util.concurrent.TimeUnit;
4658
import org.jetbrains.annotations.ApiStatus;
@@ -284,6 +296,8 @@ private void reportAsSentryEvent(
284296
}
285297
}
286298

299+
applyAnrProfile(isBackground, anrTimestamp, event);
300+
287301
final @NotNull SentryId sentryId = scopes.captureEvent(event, hint);
288302
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
289303
if (!isEventDropped) {
@@ -299,6 +313,67 @@ private void reportAsSentryEvent(
299313
}
300314
}
301315

316+
private void applyAnrProfile(
317+
final boolean isBackground, final long anrTimestamp, final @NotNull SentryEvent event) {
318+
319+
// as of now AnrProfilingIntegration only generates profiles in foreground
320+
if (isBackground) {
321+
return;
322+
}
323+
324+
@Nullable AnrProfile anrProfile = null;
325+
try {
326+
final AnrProfileManager provider = new AnrProfileManager(options);
327+
anrProfile = provider.load();
328+
} catch (Throwable t) {
329+
options.getLogger().log(SentryLevel.INFO, "Could not retrieve ANR profile");
330+
}
331+
332+
if (anrProfile != null) {
333+
options.getLogger().log(SentryLevel.INFO, "ANR profile found");
334+
// TODO maybe be less strict around the end timestamp
335+
if (anrTimestamp >= anrProfile.startTimeMs && anrTimestamp <= anrProfile.endtimeMs) {
336+
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
337+
final ProfileChunk chunk =
338+
new ProfileChunk(
339+
new SentryId(),
340+
new SentryId(),
341+
null,
342+
new HashMap<>(0),
343+
anrTimestamp / 1000.0d,
344+
ProfileChunk.PLATFORM_JAVA,
345+
options);
346+
chunk.setSentryProfile(profile);
347+
348+
options.getLogger().log(SentryLevel.DEBUG, "");
349+
scopes.captureProfileChunk(chunk);
350+
351+
final @Nullable AggregatedStackTrace culprit =
352+
AnrCulpritIdentifier.identify(anrProfile.stacks);
353+
if (culprit != null) {
354+
// TODO if quality is low (e.g. when culprit is pollNative())
355+
// consider throwing the ANR using a static fingerprint to reduce noise
356+
final @NotNull StackTraceElement[] stack = culprit.getStack();
357+
if (stack.length > 0) {
358+
final StackTraceElement stackTraceElement = culprit.getStack()[0];
359+
final String message =
360+
stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName();
361+
final AnrException exception = new AnrException(message);
362+
exception.setStackTrace(stack);
363+
364+
// TODO should this be re-used from somewhere else?
365+
final SentryExceptionFactory factory =
366+
new SentryExceptionFactory(new SentryStackTraceFactory(options));
367+
event.setExceptions(factory.getSentryExceptions(exception));
368+
event.getContexts().setProfile(new ProfileContext(chunk.getProfilerId()));
369+
}
370+
}
371+
} else {
372+
options.getLogger().log(SentryLevel.DEBUG, "ANR profile found, but doesn't match");
373+
}
374+
}
375+
}
376+
302377
private @NotNull ParseResult parseThreadDump(
303378
final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
304379
final byte[] dump;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ final class ManifestMetadataReader {
143143

144144
static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";
145145

146+
static final String ENABLE_ANR_PROFILING = "io.sentry.anr.enable-profiling";
147+
146148
/** ManifestMetadataReader ctor */
147149
private ManifestMetadataReader() {}
148150

@@ -522,6 +524,9 @@ static void applyMetadata(
522524
metadata, logger, FEEDBACK_USE_SENTRY_USER, feedbackOptions.isUseSentryUser()));
523525
feedbackOptions.setShowBranding(
524526
readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding()));
527+
528+
options.setEnableAnrProfiling(
529+
readBool(metadata, logger, ENABLE_ANR_PROFILING, options.isEnableAnrProfiling()));
525530
}
526531
options
527532
.getLogger()

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ public interface BeforeCaptureCallback {
227227

228228
private @Nullable SentryFrameMetricsCollector frameMetricsCollector;
229229

230+
private boolean enableAnrProfiling = false;
231+
230232
public SentryAndroidOptions() {
231233
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
232234
setSdkVersion(createSdkVersion());
@@ -626,6 +628,14 @@ public void setEnableSystemEventBreadcrumbsExtras(
626628
this.enableSystemEventBreadcrumbsExtras = enableSystemEventBreadcrumbsExtras;
627629
}
628630

631+
public boolean isEnableAnrProfiling() {
632+
return enableAnrProfiling;
633+
}
634+
635+
public void setEnableAnrProfiling(final boolean enableAnrProfiling) {
636+
this.enableAnrProfiling = enableAnrProfiling;
637+
}
638+
629639
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
630640
@Override
631641
public void showDialog(
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.sentry.android.core.anr;
2+
3+
import java.util.Arrays;
4+
import org.jetbrains.annotations.ApiStatus;
5+
6+
@ApiStatus.Internal
7+
public class AggregatedStackTrace {
8+
// the number of frames of the stacktrace
9+
final int depth;
10+
11+
// the quality of the stack trace, higher means better
12+
final int quality;
13+
14+
private final StackTraceElement[] stack;
15+
16+
// 0 is the most detailed frame in the stacktrace
17+
private final int stackStartIdx;
18+
private final int stackEndIdx;
19+
20+
// the total number of times this exact stacktrace was captured
21+
int count;
22+
23+
// first time the stacktrace occured
24+
private long startTimeMs;
25+
26+
// last time the stacktrace occured
27+
private long endTimeMs;
28+
29+
public AggregatedStackTrace(
30+
final StackTraceElement[] stack,
31+
final int stackStartIdx,
32+
final int stackEndIdx,
33+
final long timestampMs,
34+
final int quality) {
35+
this.stack = stack;
36+
this.stackStartIdx = stackStartIdx;
37+
this.stackEndIdx = stackEndIdx;
38+
this.depth = stackEndIdx - stackStartIdx;
39+
this.startTimeMs = timestampMs;
40+
this.endTimeMs = timestampMs;
41+
this.count = 1;
42+
this.quality = quality;
43+
}
44+
45+
public void add(long timestampMs) {
46+
this.startTimeMs = Math.min(startTimeMs, timestampMs);
47+
this.endTimeMs = Math.max(endTimeMs, timestampMs);
48+
this.count++;
49+
}
50+
51+
public StackTraceElement[] getStack() {
52+
return Arrays.copyOfRange(stack, stackStartIdx, stackEndIdx + 1);
53+
}
54+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.sentry.android.core.anr;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
@ApiStatus.Internal
13+
public class AnrCulpritIdentifier {
14+
15+
// common Java and Android packages who are less relevant for being the actual culprit
16+
private static final List<String> lowQualityPackages = new ArrayList<>(9);
17+
18+
{
19+
lowQualityPackages.add("java.lang");
20+
lowQualityPackages.add("java.util");
21+
lowQualityPackages.add("android.app");
22+
lowQualityPackages.add("android.os.Handler");
23+
lowQualityPackages.add("android.os.Looper");
24+
lowQualityPackages.add("android.view");
25+
lowQualityPackages.add("android.widget");
26+
lowQualityPackages.add("com.android.internal");
27+
lowQualityPackages.add("com.google.android");
28+
}
29+
30+
/**
31+
* @param dumps
32+
* @return
33+
*/
34+
@Nullable
35+
public static AggregatedStackTrace identify(final @NotNull List<AnrStackTrace> dumps) {
36+
if (dumps.isEmpty()) {
37+
return null;
38+
}
39+
40+
// fold all stacktraces and count their occurrences
41+
final Map<Integer, AggregatedStackTrace> stackTraceMap = new HashMap<>();
42+
for (final AnrStackTrace dump : dumps) {
43+
44+
// entry 0 is the most detailed element in the stacktrace
45+
// so create sub-stacks (1..n, 2..n, ...) to capture the most common root cause of an ANR
46+
for (int i = 0; i < dump.stack.length - 1; i++) {
47+
final int key = subArrayHashCode(dump.stack, i, dump.stack.length - 1);
48+
int quality = 10;
49+
final String clazz = dump.stack[i].getClassName();
50+
for (String ignoredPackage : lowQualityPackages) {
51+
if (clazz.startsWith(ignoredPackage)) {
52+
quality = 1;
53+
break;
54+
}
55+
}
56+
57+
@Nullable AggregatedStackTrace aggregatedStackTrace = stackTraceMap.get(key);
58+
if (aggregatedStackTrace == null) {
59+
aggregatedStackTrace =
60+
new AggregatedStackTrace(
61+
dump.stack, i, dump.stack.length - 1, dump.timestampMs, quality);
62+
stackTraceMap.put(key, aggregatedStackTrace);
63+
} else {
64+
aggregatedStackTrace.add(dump.timestampMs);
65+
}
66+
}
67+
}
68+
69+
// the deepest stacktrace with most count wins
70+
return Collections.max(
71+
stackTraceMap.values(),
72+
(c1, c2) -> {
73+
final int countComparison = Integer.compare(c1.count * c1.quality, c2.count * c2.quality);
74+
if (countComparison == 0) {
75+
return Integer.compare(c1.depth, c2.depth);
76+
}
77+
return countComparison;
78+
});
79+
}
80+
81+
private static int subArrayHashCode(
82+
final @NotNull Object[] arr, final int stackStartIdx, final int stackEndIdx) {
83+
int result = 1;
84+
for (int i = stackStartIdx; i <= stackEndIdx; i++) {
85+
final Object item = arr[i];
86+
result = 31 * result + item.hashCode();
87+
}
88+
return result;
89+
}
90+
}

0 commit comments

Comments
 (0)