package com.glview.thread; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import android.os.Process; import android.os.SystemClock; import android.util.Log; public class Watchdog extends Thread { static final String TAG = "GLWatchdog"; // Set this to true to use debug default values. static final boolean DB = false; static final long DEFAULT_TIMEOUT = DB ? 10*1000 : 60*1000; static final long CHECK_INTERVAL = DEFAULT_TIMEOUT / 2; // These are temporally ordered: larger values as lateness increases static final int COMPLETED = 0; static final int WAITING = 1; static final int WAITED_HALF = 2; static final int OVERDUE = 3; static Watchdog sWatchdog; public static Watchdog getInstance() { if (sWatchdog == null) { sWatchdog = new Watchdog(); sWatchdog.start(); } return sWatchdog; } /* This handler will be used to post message back onto the main thread */ final HashMap<Object, HandlerChecker> mHandlerCheckers = new HashMap<Object, HandlerChecker>(); /** * Used for checking status of handle threads and scheduling monitor callbacks. */ public final class HandlerChecker implements Runnable { private final Handler mHandler; private final String mName; private final long mWaitMax; private final ArrayList<Monitor> mMonitors = new ArrayList<Monitor>(); private boolean mCompleted; private Monitor mCurrentMonitor; private long mStartTime; HandlerChecker(Handler handler, String name, long waitMaxMillis) { mHandler = handler; mName = name; mWaitMax = waitMaxMillis; mCompleted = true; } public void scheduleCheckLocked() { if (mMonitors.size() == 0) { try { Method m = mHandler.getLooper().getClass().getDeclaredMethod("isIdling"); m.setAccessible(true); boolean isIdling = (Boolean)m.invoke(mHandler.getLooper()); if (isIdling) { // If the target looper is or just recently was idling, then // there is no reason to enqueue our checker on it since that // is as good as it not being deadlocked. This avoid having // to do a context switch to check the thread. Note that we // only do this if mCheckReboot is false and we have no // monitors, since those would need to be executed at this point. mCompleted = true; return; } } catch (Throwable throwable) { } } if (!mCompleted) { // we already have a check in flight, so no need return; } mCompleted = false; mCurrentMonitor = null; mStartTime = SystemClock.uptimeMillis(); mHandler.removeCallbacks(this); mHandler.postAtFrontOfQueue(this); } public boolean isOverdueLocked() { return (!mCompleted) && (SystemClock.uptimeMillis() > mStartTime + mWaitMax); } public int getCompletionStateLocked() { if (mCompleted) { return COMPLETED; } else { long latency = SystemClock.uptimeMillis() - mStartTime; if (latency < mWaitMax/2) { return WAITING; } else if (latency < mWaitMax) { return WAITED_HALF; } } return OVERDUE; } public Thread getThread() { return mHandler.getLooper().getThread(); } public String getName() { return mName; } public String describeBlockedStateLocked() { if (mCurrentMonitor == null) { return "Blocked in handler on " + mName + " (" + getThread().getName() + ")"; } else { return "Blocked in monitor " + mCurrentMonitor.getClass().getName() + " on " + mName + " (" + getThread().getName() + ")"; } } @Override public void run() { final int size = mMonitors.size(); for (int i = 0 ; i < size ; i++) { synchronized (Watchdog.this) { mCurrentMonitor = mMonitors.get(i); } mCurrentMonitor.monitor(); } synchronized (Watchdog.this) { mCompleted = true; mCurrentMonitor = null; } } } public interface Monitor { void monitor(); } private Watchdog() { super("GLWatchdog"); setDaemon(true); } public void addThread(Object token, Handler thread, String name) { addThread(token, thread, name, DEFAULT_TIMEOUT); } public void addThread(Object token, Handler thread, String name, long timeoutMillis) { synchronized (this) { /*if (isAlive()) { throw new RuntimeException("Threads can't be added once the Watchdog is running"); }*/ mHandlerCheckers.put(token, new HandlerChecker(thread, name, timeoutMillis)); } } public void removeThread(Object token) { synchronized (this) { HandlerChecker handlerChecker = mHandlerCheckers.remove(token); if (handlerChecker != null) { handlerChecker.mHandler.removeCallbacksAndMessages(null); } } } private int evaluateCheckerCompletionLocked() { int state = COMPLETED; for (Entry<Object, HandlerChecker> entry : mHandlerCheckers.entrySet()) { HandlerChecker hc = entry.getValue(); state = Math.max(state, hc.getCompletionStateLocked()); } return state; } private ArrayList<HandlerChecker> getBlockedCheckersLocked() { ArrayList<HandlerChecker> checkers = new ArrayList<HandlerChecker>(); for (Entry<Object, HandlerChecker> entry : mHandlerCheckers.entrySet()) { HandlerChecker hc = entry.getValue(); if (hc.isOverdueLocked()) { checkers.add(hc); } } return checkers; } private String describeCheckersLocked(ArrayList<HandlerChecker> checkers) { StringBuilder builder = new StringBuilder(128); for (int i=0; i<checkers.size(); i++) { if (builder.length() > 0) { builder.append(", "); } builder.append(checkers.get(i).describeBlockedStateLocked()); } return builder.toString(); } @Override public void run() { boolean waitedHalf = false; while (true) { final ArrayList<HandlerChecker> blockedCheckers; final String subject; synchronized (this) { long timeout = CHECK_INTERVAL; // Make sure we (re)spin the checkers that have become idle within // this wait-and-check interval for (Entry<Object, HandlerChecker> entry : mHandlerCheckers.entrySet()) { HandlerChecker hc = entry.getValue(); hc.scheduleCheckLocked(); } // NOTE: We use uptimeMillis() here because we do not want to increment the time we // wait while asleep. If the device is asleep then the thing that we are waiting // to timeout on is asleep as well and won't have a chance to run, causing a false // positive on when to kill things. long start = SystemClock.uptimeMillis(); while (timeout > 0) { try { wait(timeout); } catch (InterruptedException e) { Log.wtf(TAG, e); } timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start); } final int waitState = evaluateCheckerCompletionLocked(); if (waitState == COMPLETED) { // The monitors have returned; reset waitedHalf = false; continue; } else if (waitState == WAITING) { // still waiting but within their configured intervals; back off and recheck continue; } else if (waitState == WAITED_HALF) { if (!waitedHalf) { // We've waited half the deadlock-detection interval. Pull a stack // trace and wait another half. dumpStackTraces(); waitedHalf = true; } continue; } // something is overdue! blockedCheckers = getBlockedCheckersLocked(); subject = describeCheckersLocked(blockedCheckers); Log.wtf(TAG, subject); } dumpStackTraces(); // Give some extra time to make sure the stack traces get written. // The system's been hanging for a minute, another second or two won't hurt much. SystemClock.sleep(2000); // FIXME if (false) { Process.killProcess(Process.myPid()); System.exit(10); } waitedHalf = false; } } void dumpStackTraces() { try { Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces(); for (Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet()) { Thread t = entry.getKey(); StackTraceElement[] elements = entry.getValue(); Log.w(TAG, "Thread:" + t.getName() + "----------state:" + t.getState() + "---------------priority:" + t.getPriority()); for (StackTraceElement element : elements) { Log.w(TAG, "\t" + element.getClassName() + "." + element.getMethodName() + "(" + element.getLineNumber() + ")"); } Log.w(TAG, "\n"); } } catch (Throwable throwable) { Log.w(TAG, "in dumpStackTraces", throwable); } } }