Skip to content

perf(core): Skip redundant setBreadcrumbs call on the addBreadcrumb path#5690

Closed
runningcode wants to merge 1 commit into
mainfrom
no/java-606-skip-redundant-setbreadcrumbs
Closed

perf(core): Skip redundant setBreadcrumbs call on the addBreadcrumb path#5690
runningcode wants to merge 1 commit into
mainfrom
no/java-606-skip-redundant-setbreadcrumbs

Conversation

@runningcode

@runningcode runningcode commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Resolves JAVA-606.

Description

Scope.addBreadcrumb notified every scope observer twice per breadcrumb: observer.addBreadcrumb(crumb) followed by observer.setBreadcrumbs(all). For PersistingScopeObserver the second call is a guaranteed no-op right after an add — it only does work when the collection is empty (the clear case) — yet it still acquires the SynchronizedQueue lock via isEmpty() and re-iterates the observer list on every breadcrumb.

This notifies observers once per add. clearBreadcrumbs still calls setBreadcrumbs.

⚠️ Reviewer note — public API contract

IScopeObserver is a public interface. In principle a third-party observer could implement only setBreadcrumbs and rely on it being called on every add, rather than the incremental addBreadcrumb. Removing the per-add setBreadcrumbs call changes the observer notification contract on the add path.

This PR treats that as an intentional contract clarification (observers must implement addBreadcrumb for incremental updates). Flagging for a maintainer decision — if we'd rather preserve the old contract, close this in favor of documentation only. Kept as draft for that reason.

Source

Found via on-device Perfetto method-trace analysis of the Android SDK breadcrumb path. Part of Reduce SDK init time [Android].

🤖 Generated with Claude Code

Benchmark

Measured on-device with androidx Microbenchmark 1.4.1 (benchmark-junit4) on a Samsung Galaxy A55 (SM-A556B), Android API 36 (release build, non-minified).

The benchmark exercises the caller-side cost of a scope mutation with scope persistence enabled (enableScopePersistence = true, cacheDirPath set, no beforeBreadcrumb). The async serialize + disk write is dispatched to a no-op executor so it never runs on the measured thread — mirroring production, where it runs off-thread. Each branch is compared against its base commit (aab952b82e); the per-branch delta isolates this change. allocationCount is deterministic; timeNs has ~±10 ns run-to-run noise on a non-rooted device (unlocked clocks), so it is reported as a median and reproduced across 2 runs.

How it was done

A local, uncommitted androidx microbenchmark module (sentry-android-integration-tests/sentry-uitest-android-microbenchmark) depending on :sentry, with a BenchmarkRule looping the scope mutation. Run with:

./gradlew :sentry-android-integration-tests:sentry-uitest-android-microbenchmark:connectedReleaseAndroidTest \
  -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.suppressErrors=UNLOCKED

Result — scope.addBreadcrumb()

Metric main This PR Delta
allocationCount 7 7 none (expected)
timeNs (median) ~242 ns 229 ns ~-6%

As expected, dropping the redundant per-observer setBreadcrumbs call is a small time/lock win with no allocation change. The setTag control path was unchanged.

@linear-code

linear-code Bot commented Jul 2, 2026

Copy link
Copy Markdown

JAVA-606

addBreadcrumb notified each scope observer twice: addBreadcrumb(crumb)
followed by setBreadcrumbs(all). For PersistingScopeObserver the second
call is a no-op right after an add (it only acts when the collection is
empty), yet still acquires the synchronized-queue lock and re-iterates
observers on every breadcrumb. Notify observers once per add.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@runningcode runningcode force-pushed the no/java-606-skip-redundant-setbreadcrumbs branch from ce3cd6e to e6486b9 Compare July 2, 2026 16:51
@sentry

sentry Bot commented Jul 2, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.47.0 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 315.27 ms 358.67 ms 43.40 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
abfcc92 304.04 ms 370.33 ms 66.29 ms
4e3e79d 365.83 ms 477.62 ms 111.79 ms
d8912da 329.94 ms 389.68 ms 59.74 ms
d15471f 294.13 ms 399.49 ms 105.36 ms
abf451a 332.82 ms 403.67 ms 70.85 ms
604a261 380.65 ms 451.27 ms 70.62 ms
806307f 357.85 ms 424.64 ms 66.79 ms
22f4345 307.87 ms 354.51 ms 46.64 ms
d217708 375.27 ms 415.68 ms 40.41 ms
c3ee041 310.64 ms 361.90 ms 51.26 ms

App size

Revision Plain With Sentry Diff
abfcc92 1.58 MiB 2.13 MiB 557.31 KiB
4e3e79d 0 B 0 B 0 B
d8912da 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
abf451a 1.58 MiB 2.20 MiB 635.29 KiB
604a261 1.58 MiB 2.10 MiB 533.42 KiB
806307f 1.58 MiB 2.10 MiB 533.42 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
c3ee041 0 B 0 B 0 B

@runningcode runningcode closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant