Skip to content

Commit 94dca1d

Browse files
committed
perf(anr): make anr stack matching more specific
added early exits for method and file parsing by excluding separator characters ('.' and ':') skip string trim in favor of what should be a fairly quick and performant match on exactly whitespace + 'at' extra docs for the regex itself for future travelers
1 parent e7d22da commit 94dca1d

File tree

2 files changed

+29
-18
lines changed

2 files changed

+29
-18
lines changed

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/AppExitAnrTraceProcessor.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ import java.io.InputStreamReader
2525
*/
2626
internal object AppExitAnrTraceProcessor {
2727
private const val ANR_MAIN_THREAD_IDENTIFIED = "\"main\""
28-
private const val ANR_STACKTRACE_PREFIX = "at "
29-
private val ANR_STACK_TRACE_REGEX = Regex("^\\s+at\\s+(.*)\\.(.*)\\((.*):(\\d+)\\)$")
28+
29+
// matcher for lines like ` at some.pkg.ClassName.doSomething(ClassName.kt:732)`
30+
// * class - fully-qualified class
31+
// * method - method within class
32+
// * file - class source file
33+
// * line - line number
34+
private val ANR_STACK_TRACE_REGEX =
35+
Regex(
36+
"^\\s+at\\s+(?<class>.+)\\.(?<method>[^.]+)\\((?<file>[^:]+):(?<line>\\d+)\\)\$",
37+
)
3038
private val mainStackTraceFrames = mutableListOf<Int>()
3139
private var isProcessingMainThreadTrace = false
3240

@@ -69,8 +77,6 @@ internal object AppExitAnrTraceProcessor {
6977
)
7078
}
7179

72-
private fun isStackTraceLine(currentLine: String) = currentLine.trim().startsWith(ANR_STACKTRACE_PREFIX)
73-
7480
private fun setIsMainThreadStackTrace(line: String) {
7581
if (line.startsWith(ANR_MAIN_THREAD_IDENTIFIED)) {
7682
isProcessingMainThreadTrace = true
@@ -85,7 +91,6 @@ internal object AppExitAnrTraceProcessor {
8591
currentLine: String,
8692
) {
8793
setIsMainThreadStackTrace(currentLine)
88-
if (!isStackTraceLine(currentLine)) return
8994

9095
ANR_STACK_TRACE_REGEX.find(currentLine)?.destructured?.let { (className, symbolName, fileName, lineNumber) ->
9196
if (!isProcessingMainThreadTrace) {

platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterProcessorTest.kt

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,15 @@ class FatalIssueReporterProcessorTest {
120120

121121
@Test
122122
fun persistAppExitReport_whenUserPerceivedAnr_shouldMatchAnrReason() {
123-
assertAnrReason(
123+
assertAnrContents(
124124
descriptionFromAppExit = "Input Dispatching Timed Out",
125125
expectedMessage = "User Perceived ANR",
126126
)
127127
}
128128

129129
@Test
130130
fun persistAppExitReport_whenBroadcastReceiverAnr_shouldMatchAnrReason() {
131-
assertAnrReason(
131+
assertAnrContents(
132132
descriptionFromAppExit =
133133
"Broadcast of Intent { act=android.intent.action.MAIN " +
134134
"cmp=com.example.app/.MainActivity}",
@@ -138,7 +138,7 @@ class FatalIssueReporterProcessorTest {
138138

139139
@Test
140140
fun persistAppExitReport_whenExecutingServiceAnr_shouldMatchAnrReason() {
141-
assertAnrReason(
141+
assertAnrContents(
142142
descriptionFromAppExit =
143143
"Executing service. { act=android.intent.action.MAIN \" +\n" +
144144
" \"cmp=com.example.app/.MainActivity}",
@@ -148,71 +148,71 @@ class FatalIssueReporterProcessorTest {
148148

149149
@Test
150150
fun persistAppExitReport_whenStartServiceForegroundAnr_shouldMatchAnrReason() {
151-
assertAnrReason(
151+
assertAnrContents(
152152
descriptionFromAppExit = "Service.StartForeground() not called.{ act=android.intent.action.MAIN}",
153153
expectedMessage = "Service.startForeground() Not Called ANR",
154154
)
155155
}
156156

157157
@Test
158158
fun persistAppExitReport_whenContentProviderTimeoutAnr_shouldMatchAnrReason() {
159-
assertAnrReason(
159+
assertAnrContents(
160160
descriptionFromAppExit = "My Application. Content Provider Timeout",
161161
expectedMessage = "Content Provider ANR",
162162
)
163163
}
164164

165165
@Test
166166
fun persistAppExitReport_whenAppRegisteredTimeoutAnr_shouldMatchAnrReason() {
167-
assertAnrReason(
167+
assertAnrContents(
168168
descriptionFromAppExit = "My Application. App Registered Timeout",
169169
expectedMessage = "App Registered ANR",
170170
)
171171
}
172172

173173
@Test
174174
fun persistAppExitReport_whenShortFgsTimeoutAnr_shouldMatchAnrReason() {
175-
assertAnrReason(
175+
assertAnrContents(
176176
descriptionFromAppExit = "Foreground service ANR. Short FGS Timeout. Duration=5000ms",
177177
expectedMessage = "Short Foreground Service Timeout ANR",
178178
)
179179
}
180180

181181
@Test
182182
fun persistAppExitReport_whenSystemJobServiceTimeoutAnr_shouldMatchAnrReason() {
183-
assertAnrReason(
183+
assertAnrContents(
184184
descriptionFromAppExit = "SystemJobService. Job Service Timeout",
185185
expectedMessage = "Job Service ANR",
186186
)
187187
}
188188

189189
@Test
190190
fun persistAppExitReport_whenAppStartupTimeOut_shouldMatchAnrReason() {
191-
assertAnrReason(
191+
assertAnrContents(
192192
descriptionFromAppExit = "App start timeout. Timeout=5000ms",
193193
expectedMessage = "App Start ANR",
194194
)
195195
}
196196

197197
@Test
198198
fun persistAppExitReport_whenServiceStartTimeout_shouldMatchAnrReason() {
199-
assertAnrReason(
199+
assertAnrContents(
200200
descriptionFromAppExit = "Service start timeout. Timeout=5000ms",
201201
expectedMessage = "Service Start ANR",
202202
)
203203
}
204204

205205
@Test
206206
fun persistAppExitReport_whenBackgroundAnr_shouldMatchAnrReason() {
207-
assertAnrReason(
207+
assertAnrContents(
208208
descriptionFromAppExit = "It's full moon ANR",
209209
expectedMessage = "Undetermined ANR",
210210
)
211211
}
212212

213213
@Test
214214
fun persistAppExitReport_whenGenericAnrTimeout_shouldMatchAnrReason() {
215-
assertAnrReason(
215+
assertAnrContents(
216216
descriptionFromAppExit =
217217
"bg anr: Process " +
218218
"ProcessRecord{9707291 4609:io.bitdrift.gradletestapp/u0a207} " +
@@ -259,7 +259,7 @@ class FatalIssueReporterProcessorTest {
259259
.persistFatalIssue(any(), any(), any())
260260
}
261261

262-
private fun assertAnrReason(
262+
private fun assertAnrContents(
263263
descriptionFromAppExit: String,
264264
expectedMessage: String,
265265
) {
@@ -279,6 +279,12 @@ class FatalIssueReporterProcessorTest {
279279
assertThat(report.errors(0)).isNotNull
280280
report.errors(0)?.let { error ->
281281
assertThat(error.name).isEqualTo(expectedMessage)
282+
val frame = error.stackTrace(0)
283+
assertThat(frame).isNotNull
284+
assertThat(frame!!.sourceFile!!.line).isEqualTo(106)
285+
assertThat(frame.sourceFile!!.path).isEqualTo("FatalIssueGenerator.kt")
286+
assertThat(frame.symbolName).isEqualTo("startProcessing")
287+
assertThat(frame.className).isEqualTo("io.bitdrift.capture.FatalIssueGenerator")
282288
}
283289
}
284290

0 commit comments

Comments
 (0)