Skip to content

Commit

Permalink
refactor: ANR implementation now uses a HandlerThread and uptimeMilli…
Browse files Browse the repository at this point in the history
…s, and only reports ANRs in the

Using a HandlerThread with postDelayed and uptimeMillis will pause ANR detection when the system
enters a deep sleep. As the ANR dialog is only shown when the user dispatches a touch event, we use
whether the app is in the foreground or not as a proxy to determine whether we should count an ANR.
  • Loading branch information
fractalwrench committed Mar 22, 2019
1 parent d3cfcb6 commit 9ffc0a7
Showing 1 changed file with 47 additions and 31 deletions.
78 changes: 47 additions & 31 deletions sdk/src/main/java/com/bugsnag/android/BlockedThreadDetector.java
@@ -1,6 +1,9 @@
package com.bugsnag.android;

import android.app.ActivityManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;

Expand All @@ -27,7 +30,9 @@ interface Delegate {
final Looper looper;
final long checkIntervalMs;
final long blockedThresholdMs;
final Handler handler;
final Handler uiHandler;
final Handler watchdogHandler;
private final HandlerThread watchdogHandlerThread;
final Delegate delegate;

volatile long lastUpdateMs;
Expand All @@ -51,55 +56,66 @@ interface Delegate {
this.checkIntervalMs = checkIntervalMs;
this.looper = looper;
this.delegate = delegate;
this.handler = new Handler(looper);
}
this.uiHandler = new Handler(looper);

void updateLivenessTimestamp() {
lastUpdateMs = SystemClock.elapsedRealtime();
watchdogHandlerThread = new HandlerThread("bugsnag-anr-watchdog");
watchdogHandlerThread.start();
watchdogHandler = new Handler(watchdogHandlerThread.getLooper());
}

void start() {
updateLivenessTimestamp();
handler.post(livenessCheck);
watcherThread.start();
uiHandler.post(livenessCheck);
watchdogHandler.postDelayed(watchdogCheck, calculateNextCheckIn());
}

void updateLivenessTimestamp() {
lastUpdateMs = SystemClock.uptimeMillis();
}

final Runnable livenessCheck = new Runnable() {
@Override
public void run() {
updateLivenessTimestamp();
handler.postDelayed(this, checkIntervalMs);
uiHandler.postDelayed(this, checkIntervalMs);
}
};

final Thread watcherThread = new Thread() {
final Runnable watchdogCheck = new Runnable() {
@Override
public void run() {
while (!isInterrupted()) {
// when we would next consider the app blocked if no timestamp updates take place
long now = SystemClock.elapsedRealtime();
long nextCheckIn = Math.max(lastUpdateMs + blockedThresholdMs - now, 0);

try {
Thread.sleep(nextCheckIn); // throttle checks to the configured threshold
} catch (InterruptedException exc) {
interrupt();
}
checkIfThreadBlocked();
}
checkIfThreadBlocked();
watchdogHandler.postDelayed(this, calculateNextCheckIn());
}
};

private void checkIfThreadBlocked() {
long delta = SystemClock.elapsedRealtime() - lastUpdateMs;
long calculateNextCheckIn() {
long currentUptimeMs = SystemClock.uptimeMillis();
return Math.max(lastUpdateMs + blockedThresholdMs - currentUptimeMs, 0);
}

void checkIfThreadBlocked() {
long delta = SystemClock.uptimeMillis() - lastUpdateMs;
boolean inForeground = isInForeground();

if (delta > blockedThresholdMs) {
if (!isAlreadyBlocked) {
delegate.onThreadBlocked(looper.getThread());
}
isAlreadyBlocked = true; // prevents duplicate reports for the same ANR
} else {
isAlreadyBlocked = false;
if (inForeground && delta > blockedThresholdMs) {
if (!isAlreadyBlocked) {
delegate.onThreadBlocked(looper.getThread());
}
isAlreadyBlocked = true; // prevents duplicate reports for the same ANR
} else {
isAlreadyBlocked = false;
}
};
}

private boolean isInForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager.RunningAppProcessInfo info
= new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return info.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
} else {
return true;
}
}
}

0 comments on commit 9ffc0a7

Please sign in to comment.