|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
8 import java.io.BufferedOutputStream; |
|
9 import java.io.BufferedReader; |
|
10 import java.io.File; |
|
11 import java.io.FileInputStream; |
|
12 import java.io.FileOutputStream; |
|
13 import java.io.FileReader; |
|
14 import java.io.IOException; |
|
15 import java.io.InputStreamReader; |
|
16 import java.io.OutputStream; |
|
17 import java.io.Reader; |
|
18 import java.util.UUID; |
|
19 import java.util.regex.Pattern; |
|
20 |
|
21 import org.json.JSONObject; |
|
22 import org.mozilla.gecko.util.ThreadUtils; |
|
23 |
|
24 import android.content.BroadcastReceiver; |
|
25 import android.content.Context; |
|
26 import android.content.Intent; |
|
27 import android.content.IntentFilter; |
|
28 import android.os.Handler; |
|
29 import android.os.Looper; |
|
30 import android.util.Log; |
|
31 |
|
32 public final class ANRReporter extends BroadcastReceiver |
|
33 { |
|
34 private static final boolean DEBUG = false; |
|
35 private static final String LOGTAG = "GeckoANRReporter"; |
|
36 |
|
37 private static final String ANR_ACTION = "android.intent.action.ANR"; |
|
38 // Number of lines to search traces.txt to decide whether it's a Gecko ANR |
|
39 private static final int LINES_TO_IDENTIFY_TRACES = 10; |
|
40 // ANRs may happen because of memory pressure, |
|
41 // so don't use up too much memory here |
|
42 // Size of buffer to hold one line of text |
|
43 private static final int TRACES_LINE_SIZE = 100; |
|
44 // Size of block to use when processing traces.txt |
|
45 private static final int TRACES_BLOCK_SIZE = 2000; |
|
46 private static final String TRACES_CHARSET = "utf-8"; |
|
47 private static final String PING_CHARSET = "utf-8"; |
|
48 |
|
49 private static final ANRReporter sInstance = new ANRReporter(); |
|
50 private static int sRegisteredCount; |
|
51 private Handler mHandler; |
|
52 private volatile boolean mPendingANR; |
|
53 |
|
54 private static native boolean requestNativeStack(boolean unwind); |
|
55 private static native String getNativeStack(); |
|
56 private static native void releaseNativeStack(); |
|
57 |
|
58 public static void register(Context context) { |
|
59 if (sRegisteredCount++ != 0) { |
|
60 // Already registered |
|
61 return; |
|
62 } |
|
63 sInstance.start(context); |
|
64 } |
|
65 |
|
66 public static void unregister() { |
|
67 if (sRegisteredCount == 0) { |
|
68 Log.w(LOGTAG, "register/unregister mismatch"); |
|
69 return; |
|
70 } |
|
71 if (--sRegisteredCount != 0) { |
|
72 // Should still be registered |
|
73 return; |
|
74 } |
|
75 sInstance.stop(); |
|
76 } |
|
77 |
|
78 private void start(final Context context) { |
|
79 |
|
80 Thread receiverThread = new Thread(new Runnable() { |
|
81 @Override |
|
82 public void run() { |
|
83 Looper.prepare(); |
|
84 synchronized (ANRReporter.this) { |
|
85 mHandler = new Handler(); |
|
86 ANRReporter.this.notify(); |
|
87 } |
|
88 if (DEBUG) { |
|
89 Log.d(LOGTAG, "registering receiver"); |
|
90 } |
|
91 context.registerReceiver(ANRReporter.this, |
|
92 new IntentFilter(ANR_ACTION), |
|
93 null, |
|
94 mHandler); |
|
95 Looper.loop(); |
|
96 |
|
97 if (DEBUG) { |
|
98 Log.d(LOGTAG, "unregistering receiver"); |
|
99 } |
|
100 context.unregisterReceiver(ANRReporter.this); |
|
101 mHandler = null; |
|
102 } |
|
103 }, LOGTAG); |
|
104 |
|
105 receiverThread.setDaemon(true); |
|
106 receiverThread.start(); |
|
107 } |
|
108 |
|
109 private void stop() { |
|
110 synchronized (this) { |
|
111 while (mHandler == null) { |
|
112 try { |
|
113 wait(1000); |
|
114 if (mHandler == null) { |
|
115 // We timed out; just give up. The process is probably |
|
116 // quitting anyways, so we let the OS do the clean up |
|
117 Log.w(LOGTAG, "timed out waiting for handler"); |
|
118 return; |
|
119 } |
|
120 } catch (InterruptedException e) { |
|
121 } |
|
122 } |
|
123 } |
|
124 Looper looper = mHandler.getLooper(); |
|
125 looper.quit(); |
|
126 try { |
|
127 looper.getThread().join(); |
|
128 } catch (InterruptedException e) { |
|
129 } |
|
130 } |
|
131 |
|
132 private ANRReporter() { |
|
133 } |
|
134 |
|
135 // Return the "traces.txt" file, or null if there is no such file |
|
136 private static File getTracesFile() { |
|
137 try { |
|
138 // getprop [prop-name [default-value]] |
|
139 Process propProc = (new ProcessBuilder()) |
|
140 .command("/system/bin/getprop", "dalvik.vm.stack-trace-file") |
|
141 .redirectErrorStream(true) |
|
142 .start(); |
|
143 try { |
|
144 BufferedReader buf = new BufferedReader( |
|
145 new InputStreamReader(propProc.getInputStream()), TRACES_LINE_SIZE); |
|
146 String propVal = buf.readLine(); |
|
147 if (DEBUG) { |
|
148 Log.d(LOGTAG, "getprop returned " + String.valueOf(propVal)); |
|
149 } |
|
150 // getprop can return empty string when the prop value is empty |
|
151 // or prop is undefined, treat both cases the same way |
|
152 if (propVal != null && propVal.length() != 0) { |
|
153 File tracesFile = new File(propVal); |
|
154 if (tracesFile.isFile() && tracesFile.canRead()) { |
|
155 return tracesFile; |
|
156 } else if (DEBUG) { |
|
157 Log.d(LOGTAG, "cannot access traces file"); |
|
158 } |
|
159 } else if (DEBUG) { |
|
160 Log.d(LOGTAG, "empty getprop result"); |
|
161 } |
|
162 } finally { |
|
163 propProc.destroy(); |
|
164 } |
|
165 } catch (IOException e) { |
|
166 Log.w(LOGTAG, e); |
|
167 } catch (ClassCastException e) { |
|
168 Log.w(LOGTAG, e); // Bug 975436 |
|
169 } |
|
170 // Check most common location one last time just in case |
|
171 File tracesFile = new File("/data/anr/traces.txt"); |
|
172 if (tracesFile.isFile() && tracesFile.canRead()) { |
|
173 return tracesFile; |
|
174 } |
|
175 return null; |
|
176 } |
|
177 |
|
178 private static File getPingFile() { |
|
179 if (GeckoAppShell.getContext() == null) { |
|
180 return null; |
|
181 } |
|
182 GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile(); |
|
183 if (profile == null) { |
|
184 return null; |
|
185 } |
|
186 File profDir = profile.getDir(); |
|
187 if (profDir == null) { |
|
188 return null; |
|
189 } |
|
190 File pingDir = new File(profDir, "saved-telemetry-pings"); |
|
191 pingDir.mkdirs(); |
|
192 if (!(pingDir.exists() && pingDir.isDirectory())) { |
|
193 return null; |
|
194 } |
|
195 return new File(pingDir, UUID.randomUUID().toString()); |
|
196 } |
|
197 |
|
198 // Return true if the traces file corresponds to a Gecko ANR |
|
199 private static boolean isGeckoTraces(String pkgName, File tracesFile) { |
|
200 try { |
|
201 final String END_OF_PACKAGE_NAME = "([^a-zA-Z0-9_]|$)"; |
|
202 // Regex for finding our package name in the traces file |
|
203 Pattern pkgPattern = Pattern.compile(Pattern.quote(pkgName) + END_OF_PACKAGE_NAME); |
|
204 Pattern mangledPattern = null; |
|
205 if (!AppConstants.MANGLED_ANDROID_PACKAGE_NAME.equals(pkgName)) { |
|
206 mangledPattern = Pattern.compile(Pattern.quote( |
|
207 AppConstants.MANGLED_ANDROID_PACKAGE_NAME) + END_OF_PACKAGE_NAME); |
|
208 } |
|
209 if (DEBUG) { |
|
210 Log.d(LOGTAG, "trying to match package: " + pkgName); |
|
211 } |
|
212 BufferedReader traces = new BufferedReader( |
|
213 new FileReader(tracesFile), TRACES_BLOCK_SIZE); |
|
214 try { |
|
215 for (int count = 0; count < LINES_TO_IDENTIFY_TRACES; count++) { |
|
216 String line = traces.readLine(); |
|
217 if (DEBUG) { |
|
218 Log.d(LOGTAG, "identifying line: " + String.valueOf(line)); |
|
219 } |
|
220 if (line == null) { |
|
221 if (DEBUG) { |
|
222 Log.d(LOGTAG, "reached end of traces file"); |
|
223 } |
|
224 return false; |
|
225 } |
|
226 if (pkgPattern.matcher(line).find()) { |
|
227 // traces.txt file contains our package |
|
228 return true; |
|
229 } |
|
230 if (mangledPattern != null && mangledPattern.matcher(line).find()) { |
|
231 // traces.txt file contains our alternate package |
|
232 return true; |
|
233 } |
|
234 } |
|
235 } finally { |
|
236 traces.close(); |
|
237 } |
|
238 } catch (IOException e) { |
|
239 // meh, can't even read from it right. just return false |
|
240 } |
|
241 return false; |
|
242 } |
|
243 |
|
244 private static long getUptimeMins() { |
|
245 |
|
246 long uptimeMins = (new File("/proc/self/stat")).lastModified(); |
|
247 if (uptimeMins != 0L) { |
|
248 uptimeMins = (System.currentTimeMillis() - uptimeMins) / 1000L / 60L; |
|
249 if (DEBUG) { |
|
250 Log.d(LOGTAG, "uptime " + String.valueOf(uptimeMins)); |
|
251 } |
|
252 return uptimeMins; |
|
253 } else if (DEBUG) { |
|
254 Log.d(LOGTAG, "could not get uptime"); |
|
255 } |
|
256 return 0L; |
|
257 } |
|
258 |
|
259 /* |
|
260 a saved telemetry ping file consists of JSON in the following format, |
|
261 { |
|
262 "reason": "android-anr-report", |
|
263 "slug": "<uuid-string>", |
|
264 "payload": <json-object> |
|
265 } |
|
266 for Android ANR, our JSON payload should look like, |
|
267 { |
|
268 "ver": 1, |
|
269 "simpleMeasurements": { |
|
270 "uptime": <uptime> |
|
271 }, |
|
272 "info": { |
|
273 "reason": "android-anr-report", |
|
274 "OS": "Android", |
|
275 ... |
|
276 }, |
|
277 "androidANR": "...", |
|
278 "androidLogcat": "..." |
|
279 } |
|
280 */ |
|
281 |
|
282 private static int writePingPayload(OutputStream ping, |
|
283 String payload) throws IOException { |
|
284 byte [] data = payload.getBytes(PING_CHARSET); |
|
285 ping.write(data); |
|
286 return data.length; |
|
287 } |
|
288 |
|
289 private static void fillPingHeader(OutputStream ping, String slug) |
|
290 throws IOException { |
|
291 |
|
292 // ping file header |
|
293 byte [] data = ("{" + |
|
294 "\"reason\":\"android-anr-report\"," + |
|
295 "\"slug\":" + JSONObject.quote(slug) + "," + |
|
296 "\"payload\":").getBytes(PING_CHARSET); |
|
297 ping.write(data); |
|
298 if (DEBUG) { |
|
299 Log.d(LOGTAG, "wrote ping header, size = " + String.valueOf(data.length)); |
|
300 } |
|
301 |
|
302 // payload start |
|
303 int size = writePingPayload(ping, ("{" + |
|
304 "\"ver\":1," + |
|
305 "\"simpleMeasurements\":{" + |
|
306 "\"uptime\":" + String.valueOf(getUptimeMins()) + |
|
307 "}," + |
|
308 "\"info\":{" + |
|
309 "\"reason\":\"android-anr-report\"," + |
|
310 "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," + |
|
311 "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," + |
|
312 "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," + |
|
313 "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION)+ "," + |
|
314 "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," + |
|
315 "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + |
|
316 "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," + |
|
317 // Technically the platform build ID may be different, but we'll never know |
|
318 "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," + |
|
319 "\"locale\":" + JSONObject.quote(SysInfo.getLocale()) + "," + |
|
320 "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," + |
|
321 "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," + |
|
322 "\"arch\":" + JSONObject.quote(SysInfo.getArchABI()) + "," + |
|
323 "\"kernel_version\":" + JSONObject.quote(SysInfo.getKernelVersion()) + "," + |
|
324 "\"device\":" + JSONObject.quote(SysInfo.getDevice()) + "," + |
|
325 "\"manufacturer\":" + JSONObject.quote(SysInfo.getManufacturer()) + "," + |
|
326 "\"hardware\":" + JSONObject.quote(SysInfo.getHardware()) + |
|
327 "}," + |
|
328 "\"androidANR\":\"")); |
|
329 if (DEBUG) { |
|
330 Log.d(LOGTAG, "wrote metadata, size = " + String.valueOf(size)); |
|
331 } |
|
332 |
|
333 // We are at the start of ANR data |
|
334 } |
|
335 |
|
336 // Block is a section of the larger input stream, and we want to find pattern within |
|
337 // the stream. This is straightforward if the entire pattern is within one block; |
|
338 // however, if the pattern spans across two blocks, we have to match both the start of |
|
339 // the pattern in the first block and the end of the pattern in the second block. |
|
340 // * If pattern is found in block, this method returns the index at the end of the |
|
341 // found pattern, which must always be > 0. |
|
342 // * If pattern is not found, it returns 0. |
|
343 // * If the start of the pattern matches the end of the block, it returns a number |
|
344 // < 0, which equals the negated value of how many characters in pattern are already |
|
345 // matched; when processing the next block, this number is passed in through |
|
346 // prevIndex, and the rest of the characters in pattern are matched against the |
|
347 // start of this second block. The method returns value > 0 if the rest of the |
|
348 // characters match, or 0 if they do not. |
|
349 private static int getEndPatternIndex(String block, String pattern, int prevIndex) { |
|
350 if (pattern == null || block.length() < pattern.length()) { |
|
351 // Nothing to do |
|
352 return 0; |
|
353 } |
|
354 if (prevIndex < 0) { |
|
355 // Last block ended with a partial start; now match start of block to rest of pattern |
|
356 if (block.startsWith(pattern.substring(-prevIndex, pattern.length()))) { |
|
357 // Rest of pattern matches; return index at end of pattern |
|
358 return pattern.length() + prevIndex; |
|
359 } |
|
360 // Not a match; continue with normal search |
|
361 } |
|
362 // Did not find pattern in last block; see if entire pattern is inside this block |
|
363 int index = block.indexOf(pattern); |
|
364 if (index >= 0) { |
|
365 // Found pattern; return index at end of the pattern |
|
366 return index + pattern.length(); |
|
367 } |
|
368 // Block does not contain the entire pattern, but see if the end of the block |
|
369 // contains the start of pattern. To do that, we see if block ends with the |
|
370 // first n-1 characters of pattern, the first n-2 characters of pattern, etc. |
|
371 for (index = block.length() - pattern.length() + 1; index < block.length(); index++) { |
|
372 // Using index as a start, see if the rest of block contains the start of pattern |
|
373 if (block.charAt(index) == pattern.charAt(0) && |
|
374 block.endsWith(pattern.substring(0, block.length() - index))) { |
|
375 // Found partial match; return -(number of characters matched), |
|
376 // i.e. -1 for 1 character matched, -2 for 2 characters matched, etc. |
|
377 return index - block.length(); |
|
378 } |
|
379 } |
|
380 return 0; |
|
381 } |
|
382 |
|
383 // Copy the content of reader to ping; |
|
384 // copying stops when endPattern is found in the input stream |
|
385 private static int fillPingBlock(OutputStream ping, |
|
386 Reader reader, String endPattern) |
|
387 throws IOException { |
|
388 |
|
389 int total = 0; |
|
390 int endIndex = 0; |
|
391 char [] block = new char[TRACES_BLOCK_SIZE]; |
|
392 for (int size = reader.read(block); size >= 0; size = reader.read(block)) { |
|
393 String stringBlock = new String(block, 0, size); |
|
394 endIndex = getEndPatternIndex(stringBlock, endPattern, endIndex); |
|
395 if (endIndex > 0) { |
|
396 // Found end pattern; clip the string |
|
397 stringBlock = stringBlock.substring(0, endIndex); |
|
398 } |
|
399 String quoted = JSONObject.quote(stringBlock); |
|
400 total += writePingPayload(ping, quoted.substring(1, quoted.length() - 1)); |
|
401 if (endIndex > 0) { |
|
402 // End pattern already found; return now |
|
403 break; |
|
404 } |
|
405 } |
|
406 return total; |
|
407 } |
|
408 |
|
409 private static void fillPingFooter(OutputStream ping, |
|
410 boolean haveNativeStack) |
|
411 throws IOException { |
|
412 |
|
413 // We are at the end of ANR data |
|
414 |
|
415 int total = writePingPayload(ping, ("\"," + |
|
416 "\"androidLogcat\":\"")); |
|
417 |
|
418 try { |
|
419 // get the last 200 lines of logcat |
|
420 Process proc = (new ProcessBuilder()) |
|
421 .command("/system/bin/logcat", "-v", "threadtime", "-t", "200", "-d", "*:D") |
|
422 .redirectErrorStream(true) |
|
423 .start(); |
|
424 try { |
|
425 Reader procOut = new InputStreamReader(proc.getInputStream(), TRACES_CHARSET); |
|
426 int size = fillPingBlock(ping, procOut, null); |
|
427 if (DEBUG) { |
|
428 Log.d(LOGTAG, "wrote logcat, size = " + String.valueOf(size)); |
|
429 } |
|
430 } finally { |
|
431 proc.destroy(); |
|
432 } |
|
433 } catch (IOException e) { |
|
434 // ignore because logcat is not essential |
|
435 Log.w(LOGTAG, e); |
|
436 } |
|
437 |
|
438 if (haveNativeStack) { |
|
439 total += writePingPayload(ping, ("\"," + |
|
440 "\"androidNativeStack\":")); |
|
441 |
|
442 String nativeStack = String.valueOf(getNativeStack()); |
|
443 int size = writePingPayload(ping, nativeStack); |
|
444 if (DEBUG) { |
|
445 Log.d(LOGTAG, "wrote native stack, size = " + String.valueOf(size)); |
|
446 } |
|
447 total += size + writePingPayload(ping, "}"); |
|
448 } else { |
|
449 total += writePingPayload(ping, "\"}"); |
|
450 } |
|
451 |
|
452 byte [] data = ( |
|
453 "}").getBytes(PING_CHARSET); |
|
454 ping.write(data); |
|
455 if (DEBUG) { |
|
456 Log.d(LOGTAG, "wrote ping footer, size = " + String.valueOf(data.length + total)); |
|
457 } |
|
458 } |
|
459 |
|
460 private static void processTraces(Reader traces, File pingFile) { |
|
461 |
|
462 // Unwinding is memory intensive; only unwind if we have enough memory |
|
463 boolean haveNativeStack = requestNativeStack( |
|
464 /* unwind */ SysInfo.getMemSize() >= 640); |
|
465 try { |
|
466 OutputStream ping = new BufferedOutputStream( |
|
467 new FileOutputStream(pingFile), TRACES_BLOCK_SIZE); |
|
468 try { |
|
469 fillPingHeader(ping, pingFile.getName()); |
|
470 // Traces file has the format |
|
471 // ----- pid xxx at xxx ----- |
|
472 // Cmd line: org.mozilla.xxx |
|
473 // * stack trace * |
|
474 // ----- end xxx ----- |
|
475 // ----- pid xxx at xxx ----- |
|
476 // Cmd line: com.android.xxx |
|
477 // * stack trace * |
|
478 // ... |
|
479 // If we end the stack dump at the first end marker, |
|
480 // only Fennec stacks will be dumped |
|
481 int size = fillPingBlock(ping, traces, "\n----- end"); |
|
482 if (DEBUG) { |
|
483 Log.d(LOGTAG, "wrote traces, size = " + String.valueOf(size)); |
|
484 } |
|
485 fillPingFooter(ping, haveNativeStack); |
|
486 if (DEBUG) { |
|
487 Log.d(LOGTAG, "finished creating ping file"); |
|
488 } |
|
489 return; |
|
490 } finally { |
|
491 ping.close(); |
|
492 if (haveNativeStack) { |
|
493 releaseNativeStack(); |
|
494 } |
|
495 } |
|
496 } catch (IOException e) { |
|
497 Log.w(LOGTAG, e); |
|
498 } |
|
499 // exception; delete ping file |
|
500 if (pingFile.exists()) { |
|
501 pingFile.delete(); |
|
502 } |
|
503 } |
|
504 |
|
505 private static void processTraces(File tracesFile, File pingFile) { |
|
506 try { |
|
507 Reader traces = new InputStreamReader( |
|
508 new FileInputStream(tracesFile), TRACES_CHARSET); |
|
509 try { |
|
510 processTraces(traces, pingFile); |
|
511 } finally { |
|
512 traces.close(); |
|
513 } |
|
514 } catch (IOException e) { |
|
515 Log.w(LOGTAG, e); |
|
516 } |
|
517 } |
|
518 |
|
519 @Override |
|
520 public void onReceive(Context context, Intent intent) { |
|
521 if (mPendingANR) { |
|
522 // we already processed an ANR without getting unstuck; skip this one |
|
523 if (DEBUG) { |
|
524 Log.d(LOGTAG, "skipping duplicate ANR"); |
|
525 } |
|
526 return; |
|
527 } |
|
528 if (ThreadUtils.getUiHandler() != null) { |
|
529 mPendingANR = true; |
|
530 // detect when the main thread gets unstuck |
|
531 ThreadUtils.postToUiThread(new Runnable() { |
|
532 @Override |
|
533 public void run() { |
|
534 // okay to reset mPendingANR on main thread |
|
535 mPendingANR = false; |
|
536 if (DEBUG) { |
|
537 Log.d(LOGTAG, "yay we got unstuck!"); |
|
538 } |
|
539 } |
|
540 }); |
|
541 } |
|
542 if (DEBUG) { |
|
543 Log.d(LOGTAG, "receiving " + String.valueOf(intent)); |
|
544 } |
|
545 if (!ANR_ACTION.equals(intent.getAction())) { |
|
546 return; |
|
547 } |
|
548 |
|
549 // make sure we have a good save location first |
|
550 File pingFile = getPingFile(); |
|
551 if (DEBUG) { |
|
552 Log.d(LOGTAG, "using ping file: " + String.valueOf(pingFile)); |
|
553 } |
|
554 if (pingFile == null) { |
|
555 return; |
|
556 } |
|
557 |
|
558 File tracesFile = getTracesFile(); |
|
559 if (DEBUG) { |
|
560 Log.d(LOGTAG, "using traces file: " + String.valueOf(tracesFile)); |
|
561 } |
|
562 if (tracesFile == null) { |
|
563 return; |
|
564 } |
|
565 |
|
566 // We get ANR intents from all ANRs in the system, but we only want Gecko ANRs |
|
567 if (!isGeckoTraces(context.getPackageName(), tracesFile)) { |
|
568 if (DEBUG) { |
|
569 Log.d(LOGTAG, "traces is not Gecko ANR"); |
|
570 } |
|
571 return; |
|
572 } |
|
573 Log.i(LOGTAG, "processing Gecko ANR"); |
|
574 processTraces(tracesFile, pingFile); |
|
575 } |
|
576 } |