mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Render better crash stack traces for executors.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
public final class ExceptionUtil {
|
||||
|
||||
private ExceptionUtil() {}
|
||||
|
||||
/**
|
||||
* Joins the stack trace of the inferred call site with the original exception. This is
|
||||
* useful for when exceptions are thrown inside of asynchronous systems (like runnables in an
|
||||
* executor) where you'd otherwise lose important parts of the stack trace. This lets you save a
|
||||
* throwable at the entry point, and then combine it with any caught exceptions later.
|
||||
*
|
||||
* The resulting stack trace will look like this:
|
||||
*
|
||||
* Inferred
|
||||
* Stack
|
||||
* Trace
|
||||
* [[ ↑↑ Inferred Trace ↑↑ ]]
|
||||
* [[ ↓↓ Original Trace ↓↓ ]]
|
||||
* Original
|
||||
* Stack
|
||||
* Trace
|
||||
*
|
||||
* @return The provided original exception, for convenience.
|
||||
*/
|
||||
public static <E extends Throwable> E joinStackTrace(@NonNull E original, @NonNull Throwable inferred) {
|
||||
StackTraceElement[] originalTrace = original.getStackTrace();
|
||||
StackTraceElement[] inferredTrace = inferred.getStackTrace();
|
||||
StackTraceElement[] combinedTrace = new StackTraceElement[originalTrace.length + inferredTrace.length + 2];
|
||||
|
||||
System.arraycopy(originalTrace, 0, combinedTrace, 0, originalTrace.length);
|
||||
|
||||
combinedTrace[originalTrace.length] = new StackTraceElement("[[ ↑↑ Original Trace ↑↑ ]]", "", "", 0);
|
||||
combinedTrace[originalTrace.length + 1] = new StackTraceElement("[[ ↓↓ Inferred Trace ↓↓ ]]", "", "", 0);
|
||||
|
||||
System.arraycopy(inferredTrace, 0, combinedTrace, originalTrace.length + 2, inferredTrace.length);
|
||||
|
||||
original.setStackTrace(combinedTrace);
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
public static @NonNull String convertThrowableToString(@NonNull Throwable throwable) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
throwable.printStackTrace(new PrintStream(outputStream));
|
||||
return outputStream.toString();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.concurrent.TracingExecutor;
|
||||
import org.signal.core.util.concurrent.TracingExecutorService;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/**
|
||||
* Thread related utility functions.
|
||||
@@ -93,4 +98,12 @@ public final class ThreadUtil {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ignored) { }
|
||||
}
|
||||
|
||||
public static Executor trace(Executor executor) {
|
||||
return new TracingExecutor(executor);
|
||||
}
|
||||
|
||||
public static ExecutorService trace(ExecutorService executor) {
|
||||
return new TracingExecutorService(executor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.HandlerThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.LinkedBlockingLifoQueue;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -16,10 +17,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class SignalExecutors {
|
||||
|
||||
public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded"));
|
||||
public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(4, new NumberedThreadFactory("signal-bounded"));
|
||||
public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial"));
|
||||
public static final ExecutorService BOUNDED_IO = newCachedBoundedExecutor("signal-io-bounded", 1, 32, 30);
|
||||
public static final ExecutorService UNBOUNDED = ThreadUtil.trace(Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded")));
|
||||
public static final ExecutorService BOUNDED = ThreadUtil.trace(Executors.newFixedThreadPool(4, new NumberedThreadFactory("signal-bounded")));
|
||||
public static final ExecutorService SERIAL = ThreadUtil.trace(Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial")));
|
||||
public static final ExecutorService BOUNDED_IO = ThreadUtil.trace(newCachedBoundedExecutor("signal-io-bounded", 1, 32, 30));
|
||||
|
||||
private SignalExecutors() {}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.signal.core.util.concurrent
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* An executor that will keep track of the stack trace at the time of calling [execute] and use that to build a more useful stack trace in the event of a crash.
|
||||
*/
|
||||
internal class TracingExecutor(val wrapped: Executor) : Executor by wrapped {
|
||||
|
||||
override fun execute(command: Runnable?) {
|
||||
val callerStackTrace = Throwable()
|
||||
|
||||
wrapped.execute {
|
||||
val currentHandler: Thread.UncaughtExceptionHandler? = Thread.currentThread().uncaughtExceptionHandler
|
||||
val originalHandler: Thread.UncaughtExceptionHandler? = if (currentHandler is TracingUncaughtExceptionHandler) currentHandler.originalHandler else currentHandler
|
||||
|
||||
Thread.currentThread().uncaughtExceptionHandler = TracingUncaughtExceptionHandler(originalHandler, callerStackTrace)
|
||||
|
||||
command?.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.signal.core.util.concurrent
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
/**
|
||||
* An executor that will keep track of the stack trace at the time of calling [execute] and use that to build a more useful stack trace in the event of a crash.
|
||||
*/
|
||||
internal class TracingExecutorService(val wrapped: ExecutorService) : ExecutorService by wrapped {
|
||||
|
||||
override fun execute(command: Runnable?) {
|
||||
val callerStackTrace = Throwable()
|
||||
|
||||
wrapped.execute {
|
||||
val currentHandler: Thread.UncaughtExceptionHandler? = Thread.currentThread().uncaughtExceptionHandler
|
||||
val originalHandler: Thread.UncaughtExceptionHandler? = if (currentHandler is TracingUncaughtExceptionHandler) currentHandler.originalHandler else currentHandler
|
||||
|
||||
Thread.currentThread().uncaughtExceptionHandler = TracingUncaughtExceptionHandler(originalHandler, callerStackTrace)
|
||||
|
||||
command?.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.signal.core.util.concurrent
|
||||
|
||||
import org.signal.core.util.ExceptionUtil
|
||||
|
||||
/**
|
||||
* An uncaught exception handler that will combine a caller stack trace with the exception to print a more useful stack trace.
|
||||
*/
|
||||
internal class TracingUncaughtExceptionHandler (
|
||||
val originalHandler: Thread.UncaughtExceptionHandler?,
|
||||
private val callerStackTrace: Throwable) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
||||
val updated = ExceptionUtil.joinStackTrace(exception, callerStackTrace)
|
||||
originalHandler?.uncaughtException(thread, updated)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user