From 0065ec31c254b4eae46592f5a4a8f2fafdb180bd Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 13 Sep 2021 14:44:22 +0100 Subject: [PATCH] feat(threads): capture and report `Thread.state` for Android Runtime threads --- bugsnag-android-core/detekt-baseline.xml | 2 + .../main/java/com/bugsnag/android/Thread.java | 85 ++++++++++++++++++- .../com/bugsnag/android/ThreadInternal.kt | 2 + .../java/com/bugsnag/android/ThreadState.kt | 2 +- .../bugsnag/android/EventSerializationTest.kt | 2 +- .../com/bugsnag/android/ThreadFacadeTest.java | 10 ++- .../android/ThreadSerializationTest.kt | 4 + .../test/resources/event_serialization_5.json | 1 + .../resources/thread_serialization_0.json | 1 + .../resources/thread_serialization_1.json | 1 + .../resources/thread_serialization_2.json | 1 + .../resources/thread_serialization_3.json | 1 + .../bugsnag/android/ThreadDeserializer.java | 1 + .../bugsnag/android/EventDeserializerTest.kt | 1 + .../bugsnag/android/ThreadDeserializerTest.kt | 1 + .../bugsnag/android/ThreadSerializerTest.java | 2 +- 16 files changed, 112 insertions(+), 5 deletions(-) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index b2c62871dd..d867422231 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -20,7 +20,9 @@ MagicNumber:DefaultDelivery.kt$DefaultDelivery$429 MagicNumber:DefaultDelivery.kt$DefaultDelivery$499 MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3 + MaxLineLength:EventSerializationTest.kt$EventSerializationTest.Companion$it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, Thread.State.RUNNABLE, stacktrace, NoopLogger)) MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)" + MaxLineLength:ThreadState.kt$ThreadState$Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger) ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet<Plugin>() ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java index 90daa50e49..c0e469b2ae 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.IOException; import java.util.List; @@ -19,9 +20,10 @@ public class Thread implements JsonStream.Streamable { @NonNull String name, @NonNull ThreadType type, boolean errorReportingThread, + @NonNull Thread.State state, @NonNull Stacktrace stacktrace, @NonNull Logger logger) { - this.impl = new ThreadInternal(id, name, type, errorReportingThread, stacktrace); + this.impl = new ThreadInternal(id, name, type, errorReportingThread, state, stacktrace); this.logger = logger; } @@ -81,6 +83,25 @@ public ThreadType getType() { return impl.getType(); } + /** + * Sets the state of thread (from {@link java.lang.Thread}) + */ + public void setState(@NonNull Thread.State threadState) { + if (threadState != null) { + impl.setState(threadState); + } else { + logNull("state"); + } + } + + /** + * Gets the state of the thread (from {@link java.lang.Thread}) + */ + @NonNull + public Thread.State getState() { + return impl.getState(); + } + /** * Gets whether the thread was the thread that caused the event */ @@ -111,4 +132,66 @@ public List getStacktrace() { public void toStream(@NonNull JsonStream stream) throws IOException { impl.toStream(stream); } + + /** + * The state of a reported {@link Thread}. These states correspond directly to + * {@link java.lang.Thread.State}, except for {@code UNKNOWN} which indicates that + * a state could not be captured or mapped. + */ + public enum State { + NEW, + BLOCKED, + RUNNABLE, + TERMINATED, + TIMED_WAITING, + WAITING, + UNKNOWN; + + @NonNull + public static State forThread(@NonNull java.lang.Thread thread) { + java.lang.Thread.State state = thread.getState(); + return getState(state); + } + + /** + * An exception-safe wrapper for {@link #valueOf(String)} which also handles {@code null} + * names. This method is used in-preference to the standard {@code valueOf} as it will + * return {@link #UNKNOWN} instead of throwing an exception. + * + * @param name the name of the state constant to lookup + * @return the named {@link State} or {@link #UNKNOWN} + */ + @NonNull + public static State byName(@Nullable String name) { + if (name == null) { + return UNKNOWN; + } + + try { + return valueOf(name); + } catch (IllegalArgumentException iae) { + return UNKNOWN; + } + } + + @NonNull + private static State getState(java.lang.Thread.State state) { + switch (state) { + case NEW: + return NEW; + case BLOCKED: + return BLOCKED; + case RUNNABLE: + return RUNNABLE; + case TERMINATED: + return TERMINATED; + case TIMED_WAITING: + return TIMED_WAITING; + case WAITING: + return WAITING; + default: + return UNKNOWN; + } + } + } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt index 459c17f87e..0882742fe8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt @@ -7,6 +7,7 @@ class ThreadInternal internal constructor( var name: String, var type: ThreadType, val isErrorReportingThread: Boolean, + var state: Thread.State, stacktrace: Stacktrace ) : JsonStream.Streamable { @@ -18,6 +19,7 @@ class ThreadInternal internal constructor( writer.name("id").value(id) writer.name("name").value(name) writer.name("type").value(type.desc) + writer.name("state").value(state.name) writer.name("stacktrace") writer.beginArray() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt index ea27eb92a8..06e00268ac 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt @@ -67,7 +67,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc if (trace != null) { val stacktrace = Stacktrace(trace, projectPackages, logger) val errorThread = thread.id == currentThreadId - Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger) + Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger) } else { null } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt index a06f233667..fcc4e48c72 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt @@ -40,7 +40,7 @@ internal class EventSerializationTest { createEvent { val stacktrace = Stacktrace(arrayOf(), emptySet(), NoopLogger) it.threads.clear() - it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, stacktrace, NoopLogger)) + it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, Thread.State.RUNNABLE, stacktrace, NoopLogger)) }, // threads included diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java index 1f4c3b5cb6..e0c747b409 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java @@ -27,7 +27,15 @@ public void setUp() { logger = new InterceptingLogger(); List frames = Collections.emptyList(); stacktrace = new Stacktrace(frames); - thread = new Thread(1, "thread-2", ThreadType.ANDROID, false, stacktrace, logger); + thread = new Thread( + 1, + "thread-2", + ThreadType.ANDROID, + false, + Thread.State.RUNNABLE, + stacktrace, + logger + ); } @Test diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt index c60b0bf50d..41e2cc0506 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt @@ -24,6 +24,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, true, + Thread.State.RUNNABLE, Stacktrace( stacktrace, emptySet(), @@ -43,6 +44,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, false, + Thread.State.RUNNABLE, Stacktrace( stacktrace1, emptySet(), @@ -76,6 +78,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, true, + Thread.State.RUNNABLE, trace, NoopLogger ) @@ -98,6 +101,7 @@ internal class ThreadSerializationTest { "main-one", ThreadType.ANDROID, false, + Thread.State.RUNNABLE, trace, NoopLogger ) diff --git a/bugsnag-android-core/src/test/resources/event_serialization_5.json b/bugsnag-android-core/src/test/resources/event_serialization_5.json index 3b016294e0..b4e2dc7a76 100644 --- a/bugsnag-android-core/src/test/resources/event_serialization_5.json +++ b/bugsnag-android-core/src/test/resources/event_serialization_5.json @@ -37,6 +37,7 @@ "id": 5, "name": "main", "type": "android", + "state": "RUNNABLE", "stacktrace": [], "errorReportingThread": true } diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_0.json b/bugsnag-android-core/src/test/resources/thread_serialization_0.json index ee01654e3e..d6a34a802d 100644 --- a/bugsnag-android-core/src/test/resources/thread_serialization_0.json +++ b/bugsnag-android-core/src/test/resources/thread_serialization_0.json @@ -2,6 +2,7 @@ "id": 24, "name": "main-one", "type": "android", + "state": "RUNNABLE", "stacktrace": [ { "method": "run_func", diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_1.json b/bugsnag-android-core/src/test/resources/thread_serialization_1.json index 654bd10fe4..ec5ffc71b9 100644 --- a/bugsnag-android-core/src/test/resources/thread_serialization_1.json +++ b/bugsnag-android-core/src/test/resources/thread_serialization_1.json @@ -2,6 +2,7 @@ "id": 24, "name": "main-one", "type": "android", + "state": "RUNNABLE", "stacktrace": [ { "method": "run_func", diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_2.json b/bugsnag-android-core/src/test/resources/thread_serialization_2.json index ee01654e3e..d6a34a802d 100644 --- a/bugsnag-android-core/src/test/resources/thread_serialization_2.json +++ b/bugsnag-android-core/src/test/resources/thread_serialization_2.json @@ -2,6 +2,7 @@ "id": 24, "name": "main-one", "type": "android", + "state": "RUNNABLE", "stacktrace": [ { "method": "run_func", diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_3.json b/bugsnag-android-core/src/test/resources/thread_serialization_3.json index 654bd10fe4..ec5ffc71b9 100644 --- a/bugsnag-android-core/src/test/resources/thread_serialization_3.json +++ b/bugsnag-android-core/src/test/resources/thread_serialization_3.json @@ -2,6 +2,7 @@ "id": 24, "name": "main-one", "type": "android", + "state": "RUNNABLE", "stacktrace": [ { "method": "run_func", diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java index 4cddfc3672..fd7dae52ac 100644 --- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java +++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java @@ -32,6 +32,7 @@ public Thread deserialize(Map map) { MapUtils.getOrThrow(map, "name"), ThreadType.valueOf(type.toUpperCase(Locale.US)), errorReportingThread, + Thread.State.byName(MapUtils.getOrThrow(map, "state")), new Stacktrace(frames), logger ); diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt index 0f4141c3a2..767b6f33f4 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt @@ -56,6 +56,7 @@ class EventDeserializerTest { "id" to 52L, "type" to "reactnativejs", "name" to "thread-worker-02", + "state" to "RUNNABLE", "errorReportingThread" to true ) diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt index 71e4ac1db0..f2018e03b4 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt @@ -25,6 +25,7 @@ class ThreadDeserializerTest { map["id"] = 52L map["type"] = "reactnativejs" map["name"] = "thread-worker-02" + map["state"] = "RUNNABLE" map["errorReportingThread"] = true } diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java index 319cfadd22..2060ba0ecc 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java @@ -37,7 +37,7 @@ public void setup() { List frames = Collections.singletonList(stackframe); Stacktrace stacktrace = new Stacktrace(frames); thread = new Thread(1, "fake-thread", ThreadType.ANDROID, - true, stacktrace, NoopLogger.INSTANCE); + true, Thread.State.RUNNABLE, stacktrace, NoopLogger.INSTANCE); } @Test