Skip to content

Commit

Permalink
Add a configuration option to limit the number of threads captured
Browse files Browse the repository at this point in the history
  • Loading branch information
kstenerud committed Mar 8, 2022
1 parent 3808c41 commit 3dc7e75
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 79 deletions.
4 changes: 2 additions & 2 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
<ID>LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier, callbackState: CallbackState )</ID>
<ID>LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null )</ID>
<ID>LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int, @JvmField val sendThreads: ThreadSendPolicy )</ID>
<ID>LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement>>, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection&lt;String>, logger: Logger )</ID>
<ID>LongParameterList:ThreadState.kt$ThreadState$( allThreads: List&lt;JavaThread>, currentThread: JavaThread, exc: Throwable?, isUnhandled: Boolean, maxThreadCount: Int, projectPackages: Collection&lt;String>, logger: Logger )</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$429</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$499</ID>
<ID>MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3</ID>
<ID>MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)"</ID>
<ID>MaxLineLength:ThreadState.kt$ThreadState$Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger)</ID>
<ID>MaxLineLength:ThreadState.kt$ThreadState$"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]"</ID>
<ID>ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet&lt;Plugin>()</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@ class ThreadStateTest {
private val threadState = ThreadState(
null,
true,
1000,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
Thread.currentThread()
)
private val json = streamableToJsonArray(threadState)

private fun allThreads(): List<Thread> {
var rootGroup = Thread.currentThread().threadGroup!!
while (rootGroup.parent != null) {
rootGroup = rootGroup.parent
}

val threadCount = rootGroup.activeCount()
val threads: Array<Thread?> = arrayOfNulls(threadCount)
rootGroup.enumerate(threads)
return threads.filterNotNull()
}

/**
* Verifies that the current thread is serialised as an object, and that only this value
* contains the errorReportingThread boolean flag
Expand All @@ -50,11 +63,12 @@ class ThreadStateTest {
val state = ThreadState(
trace,
true,
1000,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
otherThread,
Thread.getAllStackTraces()
allThreads()
)
val json = streamableToJsonArray(state)
verifyCurrentThreadStructure(json, otherThread.id)
Expand All @@ -67,17 +81,20 @@ class ThreadStateTest {
@Test
fun testMissingCurrentThread() {
val currentThread = Thread.currentThread()
val missingTraces = Thread.getAllStackTraces()
missingTraces.remove(currentThread)
val allThreads = allThreads()
val missingThreads = allThreads.filter {
it.id != currentThread.id
}

val state = ThreadState(
trace,
true,
1000,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
currentThread,
missingTraces
missingThreads
)
val json = streamableToJsonArray(state)

Expand All @@ -92,35 +109,38 @@ class ThreadStateTest {
@Test
fun testHandledStacktrace() {
val currentThread = Thread.currentThread()
val allStackTraces = Thread.getAllStackTraces()
val allThreads = allThreads()
val state = ThreadState(
trace,
true,
1000,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
currentThread,
allStackTraces
allThreads
)
val json = streamableToJsonArray(state)

// find the stack trace for the current thread that was passed as a parameter
val expectedTrace = allStackTraces.filter {
it.key.id == currentThread.id
}.map { it.value }.first()
// find the stack trace for the current thread that was passed as a parameter.
// Drop the top 3 stack elements because we're capturing the trace from a different location.
val expectedTrace = allThreads.filter {
it.id == currentThread.id
}.first().stackTrace.drop(3)

verifyCurrentThreadStructure(json, currentThread.id) {

// the thread id + name should always be used
assertEquals(currentThread.name, it.getString("name"))
assertEquals(currentThread.id, it.getLong("id"))

// stacktrace should come from the thread (check same length and line numbers)
// stacktrace should come from the thread (check same line numbers)
val serialisedTrace = it.getJSONArray("stacktrace")
assertEquals(expectedTrace.size, serialisedTrace.length())
// Only check the lower trace elements due to different trace capture locations.
val traceOffset = serialisedTrace.length() - expectedTrace.size

expectedTrace.forEachIndexed { index, element ->
val jsonObject = serialisedTrace.getJSONObject(index)
val jsonObject = serialisedTrace.getJSONObject(index + traceOffset)
assertEquals(element.lineNumber, jsonObject.getInt("lineNumber"))
}
}
Expand All @@ -138,6 +158,7 @@ class ThreadStateTest {
val state = ThreadState(
exc,
true,
1000,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
Expand All @@ -164,22 +185,77 @@ class ThreadStateTest {
assertTrue(json.length() > 1)
}

/**
* Verifies that maxReportedThreads is honored in a handled error
*/
@Test
fun testHandledStacktraceMaxReportedThreads() {
val currentThread = Thread.currentThread()
val allThreads = allThreads()
val state = ThreadState(
trace,
true,
2,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
currentThread,
allThreads
)
val json = streamableToJsonArray(state)

assertEquals(-1, json.getJSONObject(2).getInt("id"))
assert(
json.getJSONObject(2).getString("name").endsWith(
" threads omitted as the maxReportedThreads limit (2) was exceeded]",
)
)
}

/**
* Verifies that maxReportedThreads is honored in an unhandled error
*/
@Test
fun testUnhandledStacktraceMaxReportedThreads() {
val currentThread = Thread.currentThread()
val exc: Throwable = RuntimeException("Whoops")

val state = ThreadState(
exc,
true,
4,
ThreadSendPolicy.ALWAYS,
Collections.emptyList(),
NoopLogger,
currentThread
)
val json = streamableToJsonArray(state)

assertEquals(-1, json.getJSONObject(4).getInt("id"))
assert(
json.getJSONObject(4).getString("name").endsWith(
" threads omitted as the maxReportedThreads limit (4) was exceeded]",
)
)
}

/**
* Test that using [ThreadSendPolicy.NEVER] ignores any stack-traces and reports an empty
* array of Threads
*/
@Test
fun testNeverPolicyNeverSendsThreads() {
val currentThread = Thread.currentThread()
val allStackTraces = Thread.getAllStackTraces()
val allThreads = allThreads()
val state = ThreadState(
trace,
true,
1000,
ThreadSendPolicy.NEVER,
Collections.emptyList(),
NoopLogger,
currentThread,
allStackTraces
allThreads
)
val json = streamableToJsonArray(state)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal class ConfigInternal(
var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
var context: String? = null

var redactedKeys: Set<String>
Expand Down Expand Up @@ -99,6 +100,7 @@ internal class ConfigInternal(
private const val DEFAULT_MAX_BREADCRUMBS = 50
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
private const val DEFAULT_MAX_REPORTED_THREADS = 200
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,32 @@ public void setMaxPersistedEvents(int maxPersistedEvents) {
}
}

/**
* Gets the maximum number of threads that will be reported with an event. Once the threshold is
* reached, all remaining threads will be omitted.
*
* By default, up to 200 threads are reported.
*/
public int getMaxReportedThreads() {
return impl.getMaxReportedThreads();
}

/**
* Sets the maximum number of threads that will be reported with an event. Once the threshold is
* reached, all remaining threads will be omitted.
*
* By default, up to 200 threads are reported.
*/
public void setMaxReportedThreads(int maxReportedThreads) {
if (maxReportedThreads >= 0) {
impl.setMaxReportedThreads(maxReportedThreads);
} else {
getLogger().e("Invalid configuration value detected. "
+ "Option maxReportedThreads should be a positive integer."
+ "Supplied value is " + maxReportedThreads);
}
}

/**
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
* reached, the oldest session will be deleted.
Expand Down
Loading

0 comments on commit 3dc7e75

Please sign in to comment.