Skip to content

Commit

Permalink
Merge dc2473a into 33c80c7
Browse files Browse the repository at this point in the history
  • Loading branch information
markushi authored Apr 3, 2023
2 parents 33c80c7 + dc2473a commit 70b22cf
Show file tree
Hide file tree
Showing 10 changed files with 452 additions and 153 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Read integration list written by sentry gradle plugin from manifest ([#2598](https://github.com/getsentry/sentry-java/pull/2598))
- Add Logcat adapter ([#2620](https://github.com/getsentry/sentry-java/pull/2620))
- Provide CPU count/frequency data as device context ([#2622](https://github.com/getsentry/sentry-java/pull/2622))
- Attach Trace Context when an ANR is detected (ANRv1) ([#2583](https://github.com/getsentry/sentry-java/pull/2583))

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import android.app.ActivityManager;
import android.content.Context;
import android.os.Debug;
import android.os.SystemClock;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

Expand All @@ -22,60 +22,83 @@ final class ANRWatchDog extends Thread {
private final boolean reportInDebug;
private final ANRListener anrListener;
private final MainLooperHandler uiHandler;
private final TimeProvider timeProvider;
/** the interval in which we check if there's an ANR, in ms */
private long pollingIntervalMs;

private final long timeoutIntervalMillis;
private final @NotNull ILogger logger;
private final AtomicLong tick = new AtomicLong(0);

private volatile long lastKnownActiveUiTimestampMs = 0;
private final AtomicBoolean reported = new AtomicBoolean(false);

private final @NotNull Context context;

@SuppressWarnings("UnnecessaryLambda")
private final Runnable ticker =
() -> {
tick.set(0);
reported.set(false);
};
private final Runnable ticker;

ANRWatchDog(
long timeoutIntervalMillis,
boolean reportInDebug,
@NotNull ANRListener listener,
@NotNull ILogger logger,
final @NotNull Context context) {
this(timeoutIntervalMillis, reportInDebug, listener, logger, new MainLooperHandler(), context);
this(
() -> SystemClock.uptimeMillis(),
timeoutIntervalMillis,
500,
reportInDebug,
listener,
logger,
new MainLooperHandler(),
context);
}

@TestOnly
ANRWatchDog(
@NotNull final TimeProvider timeProvider,
long timeoutIntervalMillis,
long pollingIntervalMillis,
boolean reportInDebug,
@NotNull ANRListener listener,
@NotNull ILogger logger,
@NotNull MainLooperHandler uiHandler,
final @NotNull Context context) {
super();

super("|ANR-WatchDog|");

this.timeProvider = timeProvider;
this.timeoutIntervalMillis = timeoutIntervalMillis;
this.pollingIntervalMs = pollingIntervalMillis;
this.reportInDebug = reportInDebug;
this.anrListener = listener;
this.timeoutIntervalMillis = timeoutIntervalMillis;
this.logger = logger;
this.uiHandler = uiHandler;
this.context = context;
this.ticker =
() -> {
lastKnownActiveUiTimestampMs = timeProvider.getCurrentTimeMillis();
reported.set(false);
};

if (timeoutIntervalMillis < (pollingIntervalMs * 2)) {
throw new IllegalArgumentException(
String.format(
"ANRWatchDog: timeoutIntervalMillis has to be at least %d ms",
pollingIntervalMs * 2));
}
}

@Override
public void run() {
setName("|ANR-WatchDog|");
// right when the watchdog gets started, let's assume there's no ANR
ticker.run();

long interval = timeoutIntervalMillis;
while (!isInterrupted()) {
boolean needPost = tick.get() == 0;
tick.addAndGet(interval);
if (needPost) {
uiHandler.post(ticker);
}
uiHandler.post(ticker);

try {
Thread.sleep(interval);
Thread.sleep(pollingIntervalMs);
} catch (InterruptedException e) {
try {
Thread.currentThread().interrupt();
Expand All @@ -90,8 +113,11 @@ public void run() {
return;
}

final long unresponsiveDurationMs =
timeProvider.getCurrentTimeMillis() - lastKnownActiveUiTimestampMs;

// If the main thread has not handled ticker, it is blocked. ANR.
if (tick.get() != 0 && !reported.get()) {
if (unresponsiveDurationMs > timeoutIntervalMillis) {
if (!reportInDebug && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
logger.log(
SentryLevel.DEBUG,
Expand All @@ -100,48 +126,49 @@ public void run() {
continue;
}

// we only raise an ANR event if the process is in ANR state.
// if ActivityManager is not available, we'll still be able to send ANRs
final ActivityManager am =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

if (am != null) {
List<ActivityManager.ProcessErrorStateInfo> processesInErrorState = null;
try {
// It can throw RuntimeException or OutOfMemoryError
processesInErrorState = am.getProcessesInErrorState();
} catch (Throwable e) {
logger.log(
SentryLevel.ERROR, "Error getting ActivityManager#getProcessesInErrorState.", e);
}
// if list is null, there's no process in ANR state.
if (processesInErrorState == null) {
continue;
}
boolean isAnr = false;
for (ActivityManager.ProcessErrorStateInfo item : processesInErrorState) {
if (item.condition == NOT_RESPONDING) {
isAnr = true;
break;
}
}
if (!isAnr) {
continue;
}
}
if (isProcessNotResponding() && reported.compareAndSet(false, true)) {
final String message =
"Application Not Responding for at least " + timeoutIntervalMillis + " ms.";

logger.log(SentryLevel.INFO, "Raising ANR");
final String message =
"Application Not Responding for at least " + timeoutIntervalMillis + " ms.";
final ApplicationNotResponding error =
new ApplicationNotResponding(message, uiHandler.getThread());
anrListener.onAppNotResponding(error);
}
}
}
}

final ApplicationNotResponding error =
new ApplicationNotResponding(message, uiHandler.getThread());
anrListener.onAppNotResponding(error);
interval = timeoutIntervalMillis;
private boolean isProcessNotResponding() {
// we only raise an ANR event if the process is in ANR state.
// if ActivityManager is not available, we'll still be able to send ANRs
final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

reported.set(true);
if (am != null) {
List<ActivityManager.ProcessErrorStateInfo> processesInErrorState = null;
try {
// It can throw RuntimeException or OutOfMemoryError
processesInErrorState = am.getProcessesInErrorState();
} catch (Throwable e) {
logger.log(SentryLevel.ERROR, "Error getting ActivityManager#getProcessesInErrorState.", e);
}
// if list is null, there's no process in ANR state.
if (processesInErrorState != null) {
for (ActivityManager.ProcessErrorStateInfo item : processesInErrorState) {
if (item.condition == NOT_RESPONDING) {
return true;
}
}
}
// when list is empty, or there's no element NOT_RESPONDING, we can assume the app is not
// blocked
return false;
}
return true;
}

@FunctionalInterface
interface TimeProvider {
long getCurrentTimeMillis();
}

public interface ANRListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import android.content.Context;
import io.sentry.Hint;
import io.sentry.IHub;
import io.sentry.ITransaction;
import io.sentry.Integration;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SpanStatus;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.hints.AbnormalExit;
import io.sentry.protocol.Mechanism;
Expand Down Expand Up @@ -95,9 +97,20 @@ void reportANR(
final SentryEvent event = new SentryEvent(anrThrowable);
event.setLevel(SentryLevel.ERROR);

// In case there's an ongoing transaction we want to finish it
// and assign the trace span context to the event
hub.configureScope(
scope -> {
@Nullable ITransaction transaction = scope.getTransaction();
if (transaction != null) {
transaction.forceFinish(SpanStatus.ABORTED, true);
event.setTransaction(transaction.getName());
event.getContexts().setTrace(transaction.getSpanContext());
}
});

final AnrHint anrHint = new AnrHint(isAppInBackground);
final Hint hint = HintUtils.createWithTypeCheckHint(anrHint);

hub.captureEvent(event, hint);
}

Expand Down
Loading

0 comments on commit 70b22cf

Please sign in to comment.