From 6af2fba93dbc61cfecc6dbe6871d04124f5ccd8e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 12 Jun 2026 12:35:32 +0200 Subject: [PATCH] perf(core): Short-circuit combined scope breadcrumbs Avoid allocating and sorting a merged breadcrumb queue when only one component scope has breadcrumbs. This keeps the full merge path for multi-scope breadcrumbs and returns the default write scope queue when all scopes are empty. Co-Authored-By: Claude --- .../java/io/sentry/CombinedScopeView.java | 27 ++++++++++++-- .../java/io/sentry/CombinedScopeViewTest.kt | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index f21f8697fa4..96482aa3833 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -171,10 +171,31 @@ public void setFingerprint(@NotNull List fingerprint) { @Override public @NotNull Queue getBreadcrumbs() { + final @NotNull Queue globalBreadcrumbs = globalScope.getBreadcrumbs(); + final @NotNull Queue isolationBreadcrumbs = isolationScope.getBreadcrumbs(); + final @NotNull Queue currentBreadcrumbs = scope.getBreadcrumbs(); + + final boolean hasGlobalBreadcrumbs = !globalBreadcrumbs.isEmpty(); + final boolean hasIsolationBreadcrumbs = !isolationBreadcrumbs.isEmpty(); + final boolean hasCurrentBreadcrumbs = !currentBreadcrumbs.isEmpty(); + + if (!hasGlobalBreadcrumbs && !hasIsolationBreadcrumbs && !hasCurrentBreadcrumbs) { + return getDefaultWriteScope().getBreadcrumbs(); + } + if (!hasIsolationBreadcrumbs && !hasCurrentBreadcrumbs) { + return globalBreadcrumbs; + } + if (!hasGlobalBreadcrumbs && !hasCurrentBreadcrumbs) { + return isolationBreadcrumbs; + } + if (!hasGlobalBreadcrumbs && !hasIsolationBreadcrumbs) { + return currentBreadcrumbs; + } + final @NotNull List allBreadcrumbs = new ArrayList<>(); - allBreadcrumbs.addAll(globalScope.getBreadcrumbs()); - allBreadcrumbs.addAll(isolationScope.getBreadcrumbs()); - allBreadcrumbs.addAll(scope.getBreadcrumbs()); + allBreadcrumbs.addAll(globalBreadcrumbs); + allBreadcrumbs.addAll(isolationBreadcrumbs); + allBreadcrumbs.addAll(currentBreadcrumbs); Collections.sort(allBreadcrumbs); final @NotNull Queue breadcrumbs = diff --git a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt index d768d6d32d6..6341ef7c514 100644 --- a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt +++ b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt @@ -11,6 +11,7 @@ import junit.framework.TestCase.assertTrue import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNotSame import kotlin.test.assertNull import kotlin.test.assertSame import org.junit.Assert.assertNotEquals @@ -72,6 +73,42 @@ class CombinedScopeViewTest { assertEquals("current 2", breadcrumbs.poll().message) } + @Test + fun `returns single non-empty breadcrumb queue directly`() { + var combined = fixture.getSut() + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global")) + assertSame(fixture.globalScope.breadcrumbs, combined.breadcrumbs) + + combined = fixture.getSut() + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation")) + assertSame(fixture.isolationScope.breadcrumbs, combined.breadcrumbs) + + combined = fixture.getSut() + fixture.scope.addBreadcrumb(Breadcrumb.info("current")) + assertSame(fixture.scope.breadcrumbs, combined.breadcrumbs) + } + + @Test + fun `returns default write scope breadcrumbs when all scopes are empty`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.CURRENT }) + + assertSame(fixture.scope.breadcrumbs, combined.breadcrumbs) + } + + @Test + fun `returns merged breadcrumb copy when multiple scopes have breadcrumbs`() { + val combined = fixture.getSut() + + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation")) + + val breadcrumbs = combined.breadcrumbs + + assertNotSame(fixture.globalScope.breadcrumbs, breadcrumbs) + assertNotSame(fixture.isolationScope.breadcrumbs, breadcrumbs) + assertEquals(2, breadcrumbs.size) + } + @Test fun `oldest breadcrumbs are dropped first`() { val options = SentryOptions().also { it.maxBreadcrumbs = 5 }