Skip to content

Commit 93141dd

Browse files
committed
Improve folding logic, cleanup tests
1 parent 49c2e20 commit 93141dd

File tree

4 files changed

+140
-90
lines changed

4 files changed

+140
-90
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,8 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
483483
}
484484

485485
public class io/sentry/android/core/anr/AggregatedStackTrace {
486-
public fun <init> ([Ljava/lang/StackTraceElement;IIJI)V
487-
public fun add (J)V
486+
public fun <init> ([Ljava/lang/StackTraceElement;IIJF)V
487+
public fun addOccurrence (J)V
488488
public fun getStack ()[Ljava/lang/StackTraceElement;
489489
}
490490

sentry-android-core/src/main/java/io/sentry/android/core/anr/AggregatedStackTrace.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
import java.util.Arrays;
44
import org.jetbrains.annotations.ApiStatus;
5+
import org.jetbrains.annotations.NotNull;
56

67
@ApiStatus.Internal
78
public class AggregatedStackTrace {
89
// the number of frames of the stacktrace
910
final int depth;
1011

11-
// the quality of the stack trace, higher means better
12-
final int quality;
12+
// the quality of the stack trace, higher means better (ratio of app frames: 0.0 to 1.0)
13+
final float quality;
1314

1415
private final StackTraceElement[] stack;
1516

@@ -20,18 +21,18 @@ public class AggregatedStackTrace {
2021
// the total number of times this exact stacktrace was captured
2122
int count;
2223

23-
// first time the stacktrace occured
24+
// first time the stacktrace occurred
2425
private long startTimeMs;
2526

26-
// last time the stacktrace occured
27+
// last time the stacktrace occurred
2728
private long endTimeMs;
2829

2930
public AggregatedStackTrace(
3031
final StackTraceElement[] stack,
3132
final int stackStartIdx,
3233
final int stackEndIdx,
3334
final long timestampMs,
34-
final int quality) {
35+
final float quality) {
3536
this.stack = stack;
3637
this.stackStartIdx = stackStartIdx;
3738
this.stackEndIdx = stackEndIdx;
@@ -42,12 +43,13 @@ public AggregatedStackTrace(
4243
this.quality = quality;
4344
}
4445

45-
public void add(long timestampMs) {
46+
public void addOccurrence(final long timestampMs) {
4647
this.startTimeMs = Math.min(startTimeMs, timestampMs);
4748
this.endTimeMs = Math.max(endTimeMs, timestampMs);
4849
this.count++;
4950
}
5051

52+
@NotNull
5153
public StackTraceElement[] getStack() {
5254
return Arrays.copyOfRange(stack, stackStartIdx, stackEndIdx + 1);
5355
}

sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrCulpritIdentifier.java

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,79 @@
1313
public class AnrCulpritIdentifier {
1414

1515
// common Java and Android packages who are less relevant for being the actual culprit
16-
private static final List<String> lowQualityPackages = new ArrayList<>(9);
16+
private static final List<String> systemAndFrameWorkPackages = new ArrayList<>(9);
1717

1818
static {
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");
19+
systemAndFrameWorkPackages.add("java.lang");
20+
systemAndFrameWorkPackages.add("java.util");
21+
systemAndFrameWorkPackages.add("android.app");
22+
systemAndFrameWorkPackages.add("android.os.Handler");
23+
systemAndFrameWorkPackages.add("android.os.Looper");
24+
systemAndFrameWorkPackages.add("android.view");
25+
systemAndFrameWorkPackages.add("android.widget");
26+
systemAndFrameWorkPackages.add("com.android.internal");
27+
systemAndFrameWorkPackages.add("com.google.android");
28+
}
29+
30+
private static final class StackTraceKey {
31+
private final @NotNull StackTraceElement[] stack;
32+
private final int startIdx;
33+
private final int endIdx;
34+
private final int hashCode;
35+
36+
StackTraceKey(final @NotNull StackTraceElement[] stack, final int startIdx, final int endIdx) {
37+
this.stack = stack;
38+
this.startIdx = startIdx;
39+
this.endIdx = endIdx;
40+
this.hashCode = computeHashCode();
41+
}
42+
43+
private int computeHashCode() {
44+
int result = 1;
45+
for (int i = startIdx; i <= endIdx; i++) {
46+
result = 31 * result + stack[i].hashCode();
47+
}
48+
return result;
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return hashCode;
54+
}
55+
56+
@Override
57+
public boolean equals(final Object obj) {
58+
if (this == obj) {
59+
return true;
60+
}
61+
if (!(obj instanceof StackTraceKey)) {
62+
return false;
63+
}
64+
65+
final @NotNull StackTraceKey other = (StackTraceKey) obj;
66+
67+
if (hashCode != other.hashCode) {
68+
return false;
69+
}
70+
71+
final int length = endIdx - startIdx + 1;
72+
final int otherLength = other.endIdx - other.startIdx + 1;
73+
if (length != otherLength) {
74+
return false;
75+
}
76+
77+
for (int i = 0; i < length; i++) {
78+
if (!stack[startIdx + i].equals(other.stack[other.startIdx + i])) {
79+
return false;
80+
}
81+
}
82+
83+
return true;
84+
}
2885
}
2986

3087
/**
31-
* @param stacks the captured stacktraces
88+
* @param stacks a list of stack traces to analyze
3289
* @return the most common occurring stacktrace identified as the culprit
3390
*/
3491
@Nullable
@@ -38,27 +95,32 @@ public static AggregatedStackTrace identify(final @NotNull List<AnrStackTrace> s
3895
}
3996

4097
// fold all stacktraces and count their occurrences
41-
final @NotNull Map<Integer, AggregatedStackTrace> stackTraceMap = new HashMap<>();
42-
for (final AnrStackTrace stackTrace : stacks) {
43-
98+
final @NotNull Map<StackTraceKey, AggregatedStackTrace> stackTraceMap = new HashMap<>();
99+
for (final @NotNull AnrStackTrace stackTrace : stacks) {
44100
if (stackTrace.stack.length < 2) {
45101
continue;
46102
}
47103

48-
// entry 0 is the most detailed element in the stacktrace
49-
// so create sub-stacks (1..n, 2..n, ...) to capture the most common root cause of an ANR
50-
for (int i = 0; i < stackTrace.stack.length - 1; i++) {
51-
// TODO using hashcode is actually a bad key
52-
final int key = subArrayHashCode(stackTrace.stack, i, stackTrace.stack.length - 1);
53-
int quality = 10;
54-
final String clazz = stackTrace.stack[i].getClassName();
55-
for (String ignoredPackage : lowQualityPackages) {
56-
if (clazz.startsWith(ignoredPackage)) {
57-
quality = 1;
58-
break;
59-
}
104+
// stack[0] is the most detailed element in the stacktrace
105+
// iterate from end to start (length-1 → 0) creating sub-stacks (i..n-1) to find the most
106+
// common root cause
107+
// count app frames from the end to compute quality scores
108+
int appFramesCount = 0;
109+
110+
for (int i = stackTrace.stack.length - 1; i >= 0; i--) {
111+
112+
final @NotNull String topMostClassName = stackTrace.stack[i].getClassName();
113+
final boolean isSystemFrame = isSystemFrame(topMostClassName);
114+
if (!isSystemFrame) {
115+
appFramesCount++;
60116
}
61117

118+
final int totalFrames = stackTrace.stack.length - i;
119+
final float quality = (float) appFramesCount / totalFrames;
120+
121+
final @NotNull StackTraceKey key =
122+
new StackTraceKey(stackTrace.stack, i, stackTrace.stack.length - 1);
123+
62124
@Nullable AggregatedStackTrace aggregatedStackTrace = stackTraceMap.get(key);
63125
if (aggregatedStackTrace == null) {
64126
aggregatedStackTrace =
@@ -70,7 +132,7 @@ public static AggregatedStackTrace identify(final @NotNull List<AnrStackTrace> s
70132
quality);
71133
stackTraceMap.put(key, aggregatedStackTrace);
72134
} else {
73-
aggregatedStackTrace.add(stackTrace.timestampMs);
135+
aggregatedStackTrace.addOccurrence(stackTrace.timestampMs);
74136
}
75137
}
76138
}
@@ -82,22 +144,16 @@ public static AggregatedStackTrace identify(final @NotNull List<AnrStackTrace> s
82144
// the deepest stacktrace with most count wins
83145
return Collections.max(
84146
stackTraceMap.values(),
85-
(c1, c2) -> {
86-
final int countComparison = Integer.compare(c1.count * c1.quality, c2.count * c2.quality);
87-
if (countComparison == 0) {
88-
return Integer.compare(c1.depth, c2.depth);
89-
}
90-
return countComparison;
91-
});
147+
(c1, c2) ->
148+
Float.compare(c1.count * c1.quality * c1.depth, c2.count * c2.quality * c2.depth));
92149
}
93150

94-
private static int subArrayHashCode(
95-
final @NotNull Object[] arr, final int stackStartIdx, final int stackEndIdx) {
96-
int result = 1;
97-
for (int i = stackStartIdx; i <= stackEndIdx; i++) {
98-
final Object item = arr[i];
99-
result = 31 * result + item.hashCode();
151+
private static boolean isSystemFrame(final @NotNull String clazz) {
152+
for (final String systemPackage : systemAndFrameWorkPackages) {
153+
if (clazz.startsWith(systemPackage)) {
154+
return true;
155+
}
100156
}
101-
return result;
157+
return false;
102158
}
103159
}

0 commit comments

Comments
 (0)