Skip to content

Commit d0cef6a

Browse files
authored
Merge pull request #2384 from bugsnag/PLAT-15492/improve-oom-reporting
New OOM Handling Options
2 parents e02cce0 + 00a75d0 commit d0cef6a

File tree

30 files changed

+544
-23
lines changed

30 files changed

+544
-23
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## TBD
4+
5+
### Enhancements
6+
7+
* Added `NativeOutOfMemoryPlugin` as a new way to report `OutOfMemoryError`s that uses pre-allocated memory in the NDK module instead of allocating an `Event` object. When used `OutOfMemoryError`s will be more reliably reported, but will not be passed to `OnErrorCallback`s (`OnSendCallback` works as expected).
8+
[#2384](https://github.com/bugsnag/bugsnag-android/pull/2384)
9+
310
## 6.24.0 (2026-02-11)
411

512
### Enhancements

bugsnag-android-core/api/bugsnag-android-core.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public final class com/bugsnag/android/Bugsnag {
8888
public static fun getLastRunInfo ()Lcom/bugsnag/android/LastRunInfo;
8989
public static fun getMetadata (Ljava/lang/String;)Ljava/util/Map;
9090
public static fun getMetadata (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
91+
public static fun getOutOfMemoryHandler ()Lcom/bugsnag/android/OutOfMemoryHandler;
9192
public static fun getUser ()Lcom/bugsnag/android/User;
9293
public static fun isStarted ()Z
9394
public static fun leaveBreadcrumb (Ljava/lang/String;)V
@@ -103,6 +104,7 @@ public final class com/bugsnag/android/Bugsnag {
103104
public static fun resumeSession ()Z
104105
public static fun setContext (Ljava/lang/String;)V
105106
public static fun setGroupingDiscriminator (Ljava/lang/String;)Ljava/lang/String;
107+
public static fun setOutOfMemoryHandler (Lcom/bugsnag/android/OutOfMemoryHandler;)V
106108
public static fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
107109
public static fun start (Landroid/content/Context;)Lcom/bugsnag/android/Client;
108110
public static fun start (Landroid/content/Context;Lcom/bugsnag/android/Configuration;)Lcom/bugsnag/android/Client;
@@ -147,6 +149,7 @@ public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com
147149
public fun getLastRunInfo ()Lcom/bugsnag/android/LastRunInfo;
148150
public fun getMetadata (Ljava/lang/String;)Ljava/util/Map;
149151
public fun getMetadata (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
152+
public fun getOutOfMemoryHandler ()Lcom/bugsnag/android/OutOfMemoryHandler;
150153
public fun getUser ()Lcom/bugsnag/android/User;
151154
public fun leaveBreadcrumb (Ljava/lang/String;)V
152155
public fun leaveBreadcrumb (Ljava/lang/String;Ljava/util/Map;Lcom/bugsnag/android/BreadcrumbType;)V
@@ -161,6 +164,7 @@ public class com/bugsnag/android/Client : com/bugsnag/android/CallbackAware, com
161164
public fun resumeSession ()Z
162165
public fun setContext (Ljava/lang/String;)V
163166
public fun setGroupingDiscriminator (Ljava/lang/String;)Ljava/lang/String;
167+
public fun setOutOfMemoryHandler (Lcom/bugsnag/android/OutOfMemoryHandler;)V
164168
public fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
165169
public fun startSession ()V
166170
}
@@ -700,6 +704,10 @@ public abstract interface class com/bugsnag/android/OnSessionCallback {
700704
public abstract fun onSession (Lcom/bugsnag/android/Session;)Z
701705
}
702706

707+
public abstract interface class com/bugsnag/android/OutOfMemoryHandler {
708+
public abstract fun onOutOfMemory (Ljava/lang/OutOfMemoryError;)Z
709+
}
710+
703711
public abstract interface class com/bugsnag/android/Plugin {
704712
public abstract fun load (Lcom/bugsnag/android/Client;)V
705713
public abstract fun unload ()V

bugsnag-android-core/src/main/java/com/bugsnag/android/Bugsnag.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,26 @@ public static void clearFeatureFlags() {
498498
getClient().clearFeatureFlags();
499499
}
500500

501+
/**
502+
* Override or intercept the default error handling for {@link OutOfMemoryError}s.
503+
*
504+
* @param handler the new handler to use (or null to revert to normal error handling for OOMs)
505+
* @see #getOutOfMemoryHandler()
506+
*/
507+
public static void setOutOfMemoryHandler(@Nullable OutOfMemoryHandler handler) {
508+
getClient().setOutOfMemoryHandler(handler);
509+
}
510+
511+
/**
512+
* Return the currently defined {@link OutOfMemoryHandler} if one is being used.
513+
*
514+
* @return the current {@code OutOfMemoryHandler} or null if none is set
515+
*/
516+
@Nullable
517+
public static OutOfMemoryHandler getOutOfMemoryHandler() {
518+
return getClient().getOutOfMemoryHandler();
519+
}
520+
501521
/**
502522
* Get the current Bugsnag Client instance.
503523
*/

bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,31 @@ void notifyInternalWithErrorOptions(@NonNull Event event,
938938
}
939939
}
940940

941+
/**
942+
* Override or intercept the default error handling for {@link OutOfMemoryError}s.
943+
*
944+
* @param handler the new handler to use (or null to revert to normal error handling for OOMs)
945+
* @see #getOutOfMemoryHandler()
946+
*/
947+
public void setOutOfMemoryHandler(@Nullable OutOfMemoryHandler handler) {
948+
if (exceptionHandler != null) {
949+
exceptionHandler.setOutOfMemoryHandler(handler);
950+
}
951+
}
952+
953+
/**
954+
* Return the currently defined {@link OutOfMemoryHandler} if one is being used.
955+
*
956+
* @return the current {@code OutOfMemoryHandler} or null if none is set
957+
*/
958+
@Nullable
959+
public OutOfMemoryHandler getOutOfMemoryHandler() {
960+
if (exceptionHandler == null) {
961+
return null;
962+
}
963+
return exceptionHandler.getOutOfMemoryHandler();
964+
}
965+
941966
/**
942967
* Returns the current buffer of breadcrumbs that will be sent with captured events. This
943968
* ordered list represents the most recent breadcrumbs to be captured up to the limit

bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ExceptionHandler implements UncaughtExceptionHandler {
2121
private final Logger logger;
2222
private boolean enabled = true;
2323

24+
private OutOfMemoryHandler outOfMemoryHandler = null;
25+
2426
ExceptionHandler(Client client, Logger logger) {
2527
this.client = client;
2628
this.logger = logger;
@@ -37,13 +39,26 @@ void uninstall() {
3739
Thread.setDefaultUncaughtExceptionHandler(originalHandler);
3840
}
3941

42+
public void setOutOfMemoryHandler(OutOfMemoryHandler outOfMemoryHandler) {
43+
this.outOfMemoryHandler = outOfMemoryHandler;
44+
}
45+
46+
public OutOfMemoryHandler getOutOfMemoryHandler() {
47+
return outOfMemoryHandler;
48+
}
49+
4050
@Override
4151
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
4252
try {
4353
if (!enabled || client.getConfig().shouldDiscardError(throwable)) {
4454
return;
4555
}
4656

57+
if (throwable instanceof OutOfMemoryError
58+
&& tryHandleOutOfMemory((OutOfMemoryError) throwable)) {
59+
return;
60+
}
61+
4762
boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable);
4863

4964
// Notify any subscribed clients of the uncaught exception
@@ -88,4 +103,13 @@ private void forwardToOriginalHandler(@NonNull Thread thread, @NonNull Throwable
88103
logger.w("Exception", throwable);
89104
}
90105
}
106+
107+
private boolean tryHandleOutOfMemory(OutOfMemoryError oom) {
108+
OutOfMemoryHandler handler = outOfMemoryHandler;
109+
if (handler == null) {
110+
return false;
111+
}
112+
113+
return handler.onOutOfMemory(oom);
114+
}
91115
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.bugsnag.android
2+
3+
fun interface OutOfMemoryHandler {
4+
/**
5+
* Called when an `OutOfMemoryError` is reported but before it is handled by Bugsnag. This
6+
* can be used to either fully-process the `OutOfMemoryError` on a safe path, or can
7+
* attempt to free more memory to allow the `OutOfMemoryError` error to be processed and
8+
* reported (as a normal [Event]).
9+
*
10+
* @return true if the `OutOfMemoryError` was fully reported by this handler, `false` if normal
11+
* error reporting should continue
12+
*/
13+
fun onOutOfMemory(oom: OutOfMemoryError): Boolean
14+
}

bugsnag-android-core/src/main/java/com/bugsnag/android/ThrowableExtensions.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
@file:JvmName("ThrowableUtils")
22
package com.bugsnag.android
33

4+
/**
5+
* The maximum number of causes to consider when unrolling the cause chain for a Throwable.
6+
*/
7+
private const val MAX_CAUSE_COUNT = 100
8+
49
/**
510
* Unroll the list of causes for this Throwable, handling any recursion that may appear within
611
* the chain. The first element returned will be this Throwable, and the last will be the root
@@ -17,3 +22,30 @@ internal fun Throwable.safeUnrollCauses(): List<Throwable> {
1722

1823
return causes.toList()
1924
}
25+
26+
internal inline fun Throwable.anyCauseMatches(action: (Throwable) -> Boolean): Boolean {
27+
var current: Throwable? = this
28+
var slow: Throwable? = this
29+
var advanceSlow = false
30+
var depth = 0
31+
32+
while (current != null && depth < MAX_CAUSE_COUNT) {
33+
if (action(current)) {
34+
return true
35+
}
36+
37+
current = current.cause
38+
39+
// Floyd's cycle detection (no allocation required)
40+
if (advanceSlow) {
41+
slow = slow?.cause
42+
if (current === slow) {
43+
return false
44+
}
45+
}
46+
advanceSlow = !advanceSlow
47+
depth++
48+
}
49+
50+
return false
51+
}

bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import com.bugsnag.android.NoopLogger
2121
import com.bugsnag.android.Session
2222
import com.bugsnag.android.Telemetry
2323
import com.bugsnag.android.ThreadSendPolicy
24+
import com.bugsnag.android.anyCauseMatches
2425
import com.bugsnag.android.errorApiHeaders
2526
import com.bugsnag.android.internal.dag.Provider
2627
import com.bugsnag.android.internal.dag.ValueProvider
27-
import com.bugsnag.android.safeUnrollCauses
2828
import com.bugsnag.android.sessionApiHeaders
2929
import java.io.File
3030
import java.util.regex.Pattern
@@ -132,7 +132,7 @@ data class ImmutableConfig(
132132
*/
133133
@VisibleForTesting
134134
internal fun shouldDiscardByErrorClass(exc: Throwable): Boolean {
135-
return exc.safeUnrollCauses().any { throwable ->
135+
return exc.anyCauseMatches { throwable ->
136136
val errorClass = throwable.javaClass.name
137137
shouldDiscardByErrorClass(errorClass)
138138
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.bugsnag.android
2+
3+
import android.os.RemoteException
4+
import org.junit.Assert.assertFalse
5+
import org.junit.Assert.assertTrue
6+
import org.junit.Test
7+
import java.io.IOException
8+
import java.util.concurrent.ExecutionException
9+
10+
class ThrowableCauseMatchingTest {
11+
@Test
12+
fun matchSingle() {
13+
val root = RuntimeException()
14+
assertTrue(root.anyCauseMatches { it is RuntimeException })
15+
assertFalse(root.anyCauseMatches { it is RemoteException })
16+
}
17+
18+
@Test
19+
fun simpleChain() {
20+
val root = IOException()
21+
val wrapper1 = RuntimeException(root)
22+
val wrapper2 = ExecutionException(wrapper1)
23+
24+
assertTrue(wrapper2.anyCauseMatches { it is IOException })
25+
assertTrue(wrapper2.anyCauseMatches { it is RuntimeException })
26+
assertTrue(wrapper2.anyCauseMatches { it is ExecutionException })
27+
assertFalse(wrapper2.anyCauseMatches { it is RemoteException })
28+
}
29+
30+
@Test
31+
fun complexRecursiveChain() {
32+
val root = IOException("Root cause")
33+
val wrapper1 = RuntimeException("Wrapper 1", root)
34+
val wrapper2 = ExecutionException("Wrapper 2", wrapper1)
35+
val wrapper3 = IllegalStateException("Wrapper 3", wrapper2)
36+
val wrapper4 = IllegalArgumentException("Wrapper 4")
37+
wrapper4.initCause(wrapper3)
38+
39+
// Create circular reference: root points back to wrapper4
40+
root.initCause(wrapper4)
41+
42+
assertTrue(wrapper4.anyCauseMatches { it is IOException })
43+
assertTrue(wrapper4.anyCauseMatches { it is RuntimeException })
44+
assertTrue(wrapper4.anyCauseMatches { it is ExecutionException })
45+
assertTrue(wrapper4.anyCauseMatches { it is IllegalStateException })
46+
assertTrue(wrapper4.anyCauseMatches { it is IllegalArgumentException })
47+
assertFalse(wrapper4.anyCauseMatches { it is RemoteException })
48+
}
49+
}

bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
public final class com/bugsnag/android/NativeOutOfMemoryPlugin : com/bugsnag/android/OutOfMemoryHandler, com/bugsnag/android/Plugin {
2+
public fun <init> ()V
3+
public fun load (Lcom/bugsnag/android/Client;)V
4+
public fun onOutOfMemory (Ljava/lang/OutOfMemoryError;)Z
5+
public fun unload ()V
6+
}
7+
18
public final class com/bugsnag/android/ndk/BugsnagNDK {
29
public static final field INSTANCE Lcom/bugsnag/android/ndk/BugsnagNDK;
310
public static final fun refreshSymbolTable ()V
@@ -27,6 +34,7 @@ public final class com/bugsnag/android/ndk/NativeBridge : com/bugsnag/android/in
2734
public final fun pausedSession ()V
2835
public final fun refreshSymbolTable ()V
2936
public final fun removeMetadata (Ljava/lang/String;Ljava/lang/String;)V
37+
public final fun reportOutOfMemory (Ljava/lang/OutOfMemoryError;)V
3038
public final fun setInternalMetricsEnabled (Z)V
3139
public final fun setStaticJsonData (Ljava/lang/String;)V
3240
public final fun startedSession (Ljava/lang/String;Ljava/lang/String;II)V

0 commit comments

Comments
 (0)