Problem
Scope.addBreadcrumb notifies every scope observer twice per breadcrumb:
for (final IScopeObserver observer : options.getScopeObservers()) {
observer.addBreadcrumb(breadcrumb);
observer.setBreadcrumbs(breadcrumbs); // <-- redundant right after an add
}
For PersistingScopeObserver, setBreadcrumbs is a guaranteed no-op right after an add — it only does work when the collection is empty (the clear case), and its own comment says so. But it still acquires the SynchronizedQueue lock via isEmpty() and iterates the whole observer list a second time on every breadcrumb.
Evidence
From on-device method trace analysis (median.perfetto-trace): breadcrumb adds go through the observer loop on every call. Removing the second per-observer notification halves the observer iteration + drops a redundant lock acquisition per breadcrumb.
⚠️ Risk — public API contract
IScopeObserver is a public interface. A third-party observer could implement only setBreadcrumbs (relying on it being called on every add) rather than the incremental addBreadcrumb. Removing the call changes the observer notification contract on the add path.
Options to de-risk:
- Keep both notifications but document the redundancy, OR
- Remove the
setBreadcrumbs call on the add path and treat as an intentional, documented contract clarification (observers must implement addBreadcrumb for incremental updates).
Needs a maintainer decision on the contract before merging. Draft PR removes the call with this caveat flagged.
Fix (proposed)
for (final IScopeObserver observer : options.getScopeObservers()) {
observer.addBreadcrumb(breadcrumb);
}
Acceptance
Observer notified once per breadcrumb add; clearBreadcrumbs still calls setBreadcrumbs; API-contract decision recorded on the PR.
Problem
Scope.addBreadcrumbnotifies every scope observer twice per breadcrumb:For
PersistingScopeObserver,setBreadcrumbsis a guaranteed no-op right after an add — it only does work when the collection is empty (the clear case), and its own comment says so. But it still acquires theSynchronizedQueuelock viaisEmpty()and iterates the whole observer list a second time on every breadcrumb.Evidence
From on-device method trace analysis (
median.perfetto-trace): breadcrumb adds go through the observer loop on every call. Removing the second per-observer notification halves the observer iteration + drops a redundant lock acquisition per breadcrumb.IScopeObserveris a public interface. A third-party observer could implement onlysetBreadcrumbs(relying on it being called on every add) rather than the incrementaladdBreadcrumb. Removing the call changes the observer notification contract on the add path.Options to de-risk:
setBreadcrumbscall on the add path and treat as an intentional, documented contract clarification (observers must implementaddBreadcrumbfor incremental updates).Needs a maintainer decision on the contract before merging. Draft PR removes the call with this caveat flagged.
Fix (proposed)
Acceptance
Observer notified once per breadcrumb add;
clearBreadcrumbsstill callssetBreadcrumbs; API-contract decision recorded on the PR.