diff --git a/CHANGELOG.md b/CHANGELOG.md index d43f37ed40..4fe89039e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ * Cache results from PackageManager [#1288](https://github.com/bugsnag/bugsnag-android/pull/1288) +* Use ring buffer to store breadcrumbs + [#1286](https://github.com/bugsnag/bugsnag-android/pull/1286) + ## 5.9.4 (2021-05-26) * Unity: add methods for setting autoNotify and autoDetectAnrs diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt index 678e4009d1..d8092ece01 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbFilterTest.kt @@ -24,8 +24,7 @@ class BreadcrumbFilterTest { client = generateClient(configuration) client.leaveBreadcrumb("Hello World") - - assertEquals(1, client.breadcrumbState.store.size) + assertEquals(1, client.breadcrumbState.copy().size) } @Test @@ -36,6 +35,6 @@ class BreadcrumbFilterTest { client.leaveBreadcrumb("Hello World") - assertEquals(1, client.breadcrumbState.store.size) + assertEquals(1, client.breadcrumbState.copy().size) } } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt index 34ff644a55..1f5831ca20 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/BreadcrumbStateTest.kt @@ -31,7 +31,7 @@ class BreadcrumbStateTest { fun testClientMethods() { client = generateClient() client!!.leaveBreadcrumb("Hello World") - val store = client!!.breadcrumbState.store + val store = client!!.breadcrumbState.copy() var count = 0 for (breadcrumb in store) { diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java index f09093c666..6a460759b8 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -76,14 +76,14 @@ public void testMaxBreadcrumbs() { config.setEnabledBreadcrumbTypes(new HashSet<>(breadcrumbTypes)); config.setMaxBreadcrumbs(2); client = generateClient(config); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); client.leaveBreadcrumb("test"); client.leaveBreadcrumb("another"); client.leaveBreadcrumb("yet another"); - assertEquals(2, client.breadcrumbState.getStore().size()); + assertEquals(2, client.breadcrumbState.copy().size()); - Breadcrumb poll = client.breadcrumbState.getStore().poll(); + Breadcrumb poll = client.breadcrumbState.copy().get(0); assertEquals(BreadcrumbType.MANUAL, poll.getType()); assertEquals("another", poll.getMessage()); } @@ -129,7 +129,7 @@ public void testClientBreadcrumbRetrieval() { client = generateClient(); client.leaveBreadcrumb("Hello World"); List breadcrumbs = client.getBreadcrumbs(); - List store = new ArrayList<>(client.breadcrumbState.getStore()); + List store = new ArrayList<>(client.breadcrumbState.copy()); assertEquals(store, breadcrumbs); assertNotSame(store, breadcrumbs); } @@ -153,8 +153,8 @@ public void testBreadcrumbStoreNotModified() { breadcrumbs.clear(); // only the copy should be cleared assertTrue(breadcrumbs.isEmpty()); - assertEquals(1, client.breadcrumbState.getStore().size()); - assertEquals("Manual breadcrumb", client.breadcrumbState.getStore().remove().getMessage()); + assertEquals(1, client.breadcrumbState.copy().size()); + assertEquals("Manual breadcrumb", client.breadcrumbState.copy().get(0).getMessage()); } @Test diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java index 8ecdb7e136..ecdd889d93 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/OnBreadcrumbCallbackStateTest.java @@ -34,7 +34,7 @@ public void setUp() { breadcrumbTypes.add(BreadcrumbType.USER); configuration.setEnabledBreadcrumbTypes(breadcrumbTypes); client = generateClient(configuration); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @After @@ -45,7 +45,7 @@ public void tearDown() { @Test public void noCallback() { client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } @Test @@ -57,7 +57,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @Test @@ -69,7 +69,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } @Test @@ -87,7 +87,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { } }); client.leaveBreadcrumb("Hello"); - assertEquals(0, client.breadcrumbState.getStore().size()); + assertEquals(0, client.breadcrumbState.copy().size()); } @Test @@ -161,7 +161,7 @@ public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { client.leaveBreadcrumb("Hello"); client.removeOnBreadcrumb(cb); client.leaveBreadcrumb("Hello"); - assertEquals(1, client.breadcrumbState.getStore().size()); + assertEquals(1, client.breadcrumbState.copy().size()); } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt index a8882b8c18..331ca721a5 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BreadcrumbState.kt @@ -1,41 +1,41 @@ package com.bugsnag.android import java.io.IOException -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +/** + * Stores breadcrumbs added to the [Client] in a ring buffer. If the number of breadcrumbs exceeds + * the maximum configured limit then the oldest breadcrumb in the ring buffer will be overwritten. + * + * When the breadcrumbs are required for generation of an event a [List] is constructed and + * breadcrumbs added in the order of their addition. + */ internal class BreadcrumbState( - maxBreadcrumbs: Int, - val callbackState: CallbackState, - val logger: Logger + private val maxBreadcrumbs: Int, + private val callbackState: CallbackState, + private val logger: Logger ) : BaseObservable(), JsonStream.Streamable { - val store: Queue = ConcurrentLinkedQueue() + /* + * We use the `index` as both a pointer to the tail of our ring-buffer, and also as "cheat" + * semaphore. When the ring-buffer is being copied - the index is set to a negative number, + * which is an invalid array-index. By masking the `expected` value in a `compareAndSet` with + * `validIndexMask`: the CAS operation will only succeed if it wouldn't interrupt a concurrent + * `copy()` call. + */ + private val validIndexMask: Int = Int.MAX_VALUE - private val maxBreadcrumbs: Int - - init { - when { - maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs - else -> this.maxBreadcrumbs = 0 - } - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - pruneBreadcrumbs() - writer.beginArray() - store.forEach { it.toStream(writer) } - writer.endArray() - } + private val store = arrayOfNulls(maxBreadcrumbs) + private val index = AtomicInteger(0) fun add(breadcrumb: Breadcrumb) { - if (!callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { + if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { return } - store.add(breadcrumb) - pruneBreadcrumbs() + // store the breadcrumb in the ring buffer + val position = getBreadcrumbIndex() + store[position] = breadcrumb updateState { // use direct field access to avoid overhead of accessor method @@ -48,10 +48,49 @@ internal class BreadcrumbState( } } - private fun pruneBreadcrumbs() { - // Remove oldest breadcrumbState until new max size reached - while (store.size > maxBreadcrumbs) { - store.poll() + /** + * Retrieves the index in the ring buffer where the breadcrumb should be stored. + */ + private fun getBreadcrumbIndex(): Int { + while (true) { + val currentValue = index.get() and validIndexMask + val nextValue = (currentValue + 1) % maxBreadcrumbs + if (index.compareAndSet(currentValue, nextValue)) { + return currentValue + } + } + } + + /** + * Creates a copy of the breadcrumbs in the order of their addition. + */ + fun copy(): List { + if (maxBreadcrumbs == 0) { + return emptyList() + } + + // Set a negative value that stops any other thread from adding a breadcrumb. + // This handles reentrancy by waiting here until the old value has been reset. + var tail = -1 + while (tail == -1) { + tail = index.getAndSet(-1) } + + try { + val result = arrayOfNulls(maxBreadcrumbs) + store.copyInto(result, 0, tail, maxBreadcrumbs) + store.copyInto(result, maxBreadcrumbs - tail, 0, tail) + return result.filterNotNull() + } finally { + index.set(tail) + } + } + + @Throws(IOException::class) + override fun toStream(writer: JsonStream) { + val crumbs = copy() + writer.beginArray() + crumbs.forEach { it.toStream(writer) } + writer.endArray() } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 938afa8c38..f7ec7fd4fb 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -711,7 +711,7 @@ void populateAndNotifyAndroidEvent(@NonNull Event event, event.addMetadata("app", appDataCollector.getAppDataMetadata()); // Attach breadcrumbState to the event - event.setBreadcrumbs(new ArrayList<>(breadcrumbState.getStore())); + event.setBreadcrumbs(breadcrumbState.copy()); // Attach user info to the event User user = userState.getUser(); @@ -765,7 +765,7 @@ void notifyInternal(@NonNull Event event, */ @NonNull public List getBreadcrumbs() { - return new ArrayList<>(breadcrumbState.getStore()); + return breadcrumbState.copy(); } @NonNull diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt index 219c58d697..8153a7c669 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbStateTest.kt @@ -18,7 +18,15 @@ class BreadcrumbStateTest { @Before fun setUp() { - breadcrumbState = BreadcrumbState(20, CallbackState(), NoopLogger) + breadcrumbState = createBreadcrumbState(CallbackState()) + } + + private fun createBreadcrumbState(cbState: CallbackState): BreadcrumbState { + return BreadcrumbState( + 20, + cbState, + NoopLogger + ) } /** @@ -36,46 +44,119 @@ class BreadcrumbStateTest { ) breadcrumbState.add(Breadcrumb(longStr, NoopLogger)) - val crumbs = breadcrumbState.store.toList() + val crumbs = breadcrumbState.copy() assertEquals(3, crumbs.size) assertEquals(longStr, crumbs[2].message) } /** - * Verifies that leaving breadcrumbs drops the oldest breadcrumb after reaching the max limit + * Verifies that no breadcrumbs are added if the size limit is set to 0 */ @Test - fun testSizeLimitBeforeAdding() { + fun testZeroMaxBreadcrumbs() { + breadcrumbState = BreadcrumbState(0, CallbackState(), NoopLogger) + breadcrumbState.add(Breadcrumb("1", NoopLogger)) + breadcrumbState.add(Breadcrumb("2", NoopLogger)) + assertTrue(breadcrumbState.copy().isEmpty()) + } + + /** + * Verifies that one breadcrumb is added if the size limit is set to 1 + */ + @Test + fun testOneMaxBreadcrumb() { + breadcrumbState = BreadcrumbState(1, CallbackState(), NoopLogger) + assertTrue(breadcrumbState.copy().isEmpty()) + + breadcrumbState.add(Breadcrumb("A", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("A", breadcrumbState.copy()[0].message) + + breadcrumbState.add(Breadcrumb("B", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("B", breadcrumbState.copy()[0].message) + + breadcrumbState.add(Breadcrumb("C", NoopLogger)) + assertEquals(1, breadcrumbState.copy().size) + assertEquals("C", breadcrumbState.copy()[0].message) + } + + /** + * Verifies that the ring buffer can be filled up to the maxBreadcrumbs limit + */ + @Test + fun testRingBufferFilled() { + breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) + + for (k in 1..5) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("1", crumbs[0].message) + assertEquals("2", crumbs[1].message) + assertEquals("3", crumbs[2].message) + assertEquals("4", crumbs[3].message) + assertEquals("5", crumbs[4].message) + } + + /** + * Verifies that the breadcrumbs are in order after the ring buffer wraps around by one + */ + @Test + fun testRingBufferExceededByOne() { breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) for (k in 1..6) { breadcrumbState.add(Breadcrumb("$k", NoopLogger)) } - val crumbs = breadcrumbState.store.toList() - assertEquals("2", crumbs.first().message) - assertEquals("6", crumbs.last().message) + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("2", crumbs[0].message) + assertEquals("3", crumbs[1].message) + assertEquals("4", crumbs[2].message) + assertEquals("5", crumbs[3].message) + assertEquals("6", crumbs[4].message) } /** - * Verifies that no breadcrumbs are added if the size limit is set to 0 + * Verifies that the breadcrumbs are in order after the ring buffer wraps around by four */ @Test - fun testSetSizeEmpty() { - breadcrumbState = BreadcrumbState(0, CallbackState(), NoopLogger) - breadcrumbState.add(Breadcrumb("1", NoopLogger)) - breadcrumbState.add(Breadcrumb("2", NoopLogger)) - assertTrue(breadcrumbState.store.isEmpty()) + fun testRingBufferExceededByFour() { + breadcrumbState = BreadcrumbState(5, CallbackState(), NoopLogger) + + for (k in 1..9) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(5, crumbs.size) + assertEquals("5", crumbs[0].message) + assertEquals("6", crumbs[1].message) + assertEquals("7", crumbs[2].message) + assertEquals("8", crumbs[3].message) + assertEquals("9", crumbs[4].message) } /** - * Verifies that setting a negative size has no effect + * Verifies that the breadcrumbs are in order after the ring buffer is filled twice */ @Test - fun testSetSizeNegative() { - breadcrumbState = BreadcrumbState(-1, CallbackState(), NoopLogger) - breadcrumbState.add(Breadcrumb("1", NoopLogger)) - assertEquals(0, breadcrumbState.store.size) + fun testRingBufferFilledTwice() { + breadcrumbState = BreadcrumbState(3, CallbackState(), NoopLogger) + + for (k in 1..6) { + breadcrumbState.add(Breadcrumb("$k", NoopLogger)) + } + + val crumbs = breadcrumbState.copy() + assertEquals(3, crumbs.size) + assertEquals("4", crumbs[0].message) + assertEquals("5", crumbs[1].message) + assertEquals("6", crumbs[2].message) } /** @@ -84,7 +165,7 @@ class BreadcrumbStateTest { @Test fun testDefaultBreadcrumbType() { breadcrumbState.add(Breadcrumb("1", NoopLogger)) - val crumb = requireNotNull(breadcrumbState.store.peek()) + val crumb = breadcrumbState.copy()[0] assertEquals(MANUAL, crumb.type) } @@ -97,8 +178,16 @@ class BreadcrumbStateTest { for (i in 0..399) { metadata[String.format(Locale.US, "%d", i)] = "!!" } - breadcrumbState.add(Breadcrumb("Rotated Menu", BreadcrumbType.STATE, metadata, Date(0), NoopLogger)) - assertFalse(breadcrumbState.store.isEmpty()) + breadcrumbState.add( + Breadcrumb( + "Rotated Menu", + BreadcrumbType.STATE, + metadata, + Date(0), + NoopLogger + ) + ) + assertFalse(breadcrumbState.copy().isEmpty()) } /** @@ -125,14 +214,18 @@ class BreadcrumbStateTest { @Test fun testOnBreadcrumbCallback() { val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { - true - } + val cbState = CallbackState( + onBreadcrumbTasks = mutableListOf( + OnBreadcrumbCallback { + true + } + ) ) + breadcrumbState = createBreadcrumbState(cbState) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(breadcrumb, breadcrumbState.store.peek()) + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(breadcrumb, copy.first()) } /** @@ -140,25 +233,31 @@ class BreadcrumbStateTest { */ @Test fun testOnBreadcrumbCallbackFalse() { + val cbState = CallbackState() + breadcrumbState = createBreadcrumbState(cbState) + val requiredBreadcrumb = Breadcrumb("Hello there", NoopLogger) breadcrumbState.add(requiredBreadcrumb) - val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( + cbState.addOnBreadcrumb( OnBreadcrumbCallback { givenBreadcrumb -> givenBreadcrumb.metadata?.put("callback", "first") false } ) - breadcrumbState.callbackState.addOnBreadcrumb( + cbState.addOnBreadcrumb( OnBreadcrumbCallback { givenBreadcrumb -> givenBreadcrumb.metadata?.put("callback", "second") true } ) + + val breadcrumb = Breadcrumb("Whoops", NoopLogger) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(requiredBreadcrumb, breadcrumbState.store.first()) + + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(requiredBreadcrumb, copy.first()) assertNotNull(breadcrumb.metadata) assertEquals("first", breadcrumb.metadata?.get("callback")) } @@ -168,22 +267,37 @@ class BreadcrumbStateTest { */ @Test fun testOnBreadcrumbCallbackException() { - val breadcrumb = Breadcrumb("Whoops", NoopLogger) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { - throw IllegalStateException("Oh no") - } - ) - breadcrumbState.callbackState.addOnBreadcrumb( - OnBreadcrumbCallback { givenBreadcrumb -> - givenBreadcrumb.metadata?.put("callback", "second") - true - } + val cbState = CallbackState( + onBreadcrumbTasks = mutableListOf( + OnBreadcrumbCallback { + throw IllegalStateException("Oh no") + }, + OnBreadcrumbCallback { givenBreadcrumb -> + givenBreadcrumb.metadata?.put("callback", "second") + true + } + ) ) + breadcrumbState = createBreadcrumbState(cbState) + + val breadcrumb = Breadcrumb("Whoops", NoopLogger) breadcrumbState.add(breadcrumb) - assertEquals(1, breadcrumbState.store.size) - assertEquals(breadcrumb, breadcrumbState.store.peek()) + + val copy = breadcrumbState.copy() + assertEquals(1, copy.size) + assertEquals(breadcrumb, copy[0]) assertNotNull(breadcrumb.metadata) assertEquals("second", breadcrumb.metadata?.get("callback")) } + + @Test + fun testCopyThenAdd() { + breadcrumbState = BreadcrumbState(25, CallbackState(), NoopLogger) + + repeat(1000) { count -> + breadcrumbState.add(Breadcrumb("$count", NoopLogger)) + } + + assertEquals(25, breadcrumbState.copy().size) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java index 5685df3ded..94162c04e4 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java @@ -22,7 +22,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -148,7 +148,7 @@ public void setUp() { when(appDataCollector.generateAppWithState()).thenReturn(app); when(appDataCollector.getAppDataMetadata()).thenReturn(new HashMap()); - when(breadcrumbState.getStore()).thenReturn(new ArrayDeque()); + when(breadcrumbState.copy()).thenReturn(new ArrayList()); when(userState.getUser()).thenReturn(new User()); when(callbackState.runOnErrorTasks(any(Event.class), any(Logger.class))).thenReturn(true); } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt index bc9f051eca..9e1b65f449 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt @@ -111,7 +111,7 @@ internal class DeliveryDelegateTest { assertEquals(DeliveryStatus.DELIVERED, status) assertEquals("Sent 1 new event to Bugsnag", logger.msg) - val breadcrumb = requireNotNull(breadcrumbState.store.peek()) + val breadcrumb = requireNotNull(breadcrumbState.copy().first()) assertEquals(BreadcrumbType.ERROR, breadcrumb.type) assertEquals("java.lang.RuntimeException", breadcrumb.message) assertEquals("java.lang.RuntimeException", breadcrumb.metadata!!["errorClass"]) diff --git a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt index fb3aab1280..2778187aca 100644 --- a/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt +++ b/bugsnag-benchmarks/src/androidTest/java/com/bugsnag/android/benchmark/CriticalPathBenchmarkTest.kt @@ -94,6 +94,19 @@ class CriticalPathBenchmarkTest { } } + /** + * Make a copy of breadcrumbs on the Client (required when generating events) + */ + @Test + fun copyBreadcrumbs() { + repeat(201) { count -> + client.leaveBreadcrumb("Hello world $count") + } + benchmarkRule.measureRepeated { + client.breadcrumbs + } + } + /** * Add a single value to the Client metadata */