Patch, measurement, and rationale for every attempted change
+
Each attempt has a stable ID reused in navigation, measurements, and explanations. Measurements are medians from the autoresearch harness unless marked not measured. All deltas and percentages are computed against AR-00 baseline.
+
+
Best recorded allocation3,967,752B−120,984B / −2.96% vs baseline
+
Best recorded CPU47.970ms−12.036ms / −20.06% vs baseline
+
Best recorded wall18.248ms−4.747ms / −20.64% vs baseline
+
Output equivalence132envelopes every measured run
+
+
Patch note: kept attempts use actual commit diffs where available. Discarded attempts were reset by the autoresearch harness, so their patch blocks are labeled as reconstructed patch-style diffs when the exact uncommitted patch was no longer available.
Avoid Scope defensive copies + use HashMap in CombinedScopeView
+
+ kept
+
+
+
Allocation4,042,104B−46,632B / −1.14% vs baseline
+
CPU median47.770ms−12.236ms / −20.39% vs baseline; passed
+
Wall median20.311ms−2.684ms / −11.67% vs baseline; Relay output equivalent
+
+
+
Why try this?
Scope.getTags() and getAttributes() copied thread-safe internal maps on every read. CombinedScopeView also built short-lived merged maps as ConcurrentHashMap, even though the merged view is transient.
+
What changed?
Return the internal Scope maps directly and use HashMap for CombinedScopeView merged tags, extras, and attributes.
+
Result
Kept. Allocation improved by ~46KB and CPU/wall were faster than baseline in this run. Later safety review found the raw internal map return was not acceptable API behavior, so that portion was reverted after the experiment batch.
+
+
+ Patch-style diff actual commit 06d34e023f
+
diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java
+@@
+ import java.util.ArrayList;
++import java.util.HashMap;
+ import java.util.Collection;
+@@
+-import java.util.concurrent.ConcurrentHashMap;
+@@
+ public @NotNull Map<String, String> getTags() {
+- final @NotNull Map<String, String> allTags = new ConcurrentHashMap<>();
++ final @NotNull Map<String, String> allTags = new HashMap<>();
+@@
+ public @NotNull Map<String, SentryAttribute> getAttributes() {
+- final @NotNull Map<String, SentryAttribute> allAttributes = new ConcurrentHashMap<>();
++ final @NotNull Map<String, SentryAttribute> allAttributes = new HashMap<>();
+@@
+ public @NotNull Map<String, Object> getExtras() {
+- final @NotNull Map<String, Object> allTags = new ConcurrentHashMap<>();
++ final @NotNull Map<String, Object> allTags = new HashMap<>();
+
+diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java
+@@
+ public @NotNull Map<String, String> getTags() {
+- return CollectionUtils.newConcurrentHashMap(tags);
++ return tags;
+ }
+@@
+ public @NotNull Map<String, SentryAttribute> getAttributes() {
+- return CollectionUtils.newConcurrentHashMap(attributes);
++ return attributes;
+ }
Use ArrayDeque for CombinedScopeView breadcrumbs merge
+
+ discarded
+
+
+
Allocation4,034,544B−54,192B / −1.33% vs baseline
+
CPU median58.561ms−1.445ms / −2.41% vs baseline; failed guardrail
+
Wall median26.926ms+3.931ms / +17.10% vs baseline; Relay output equivalent
+
+
+
Why try this?
The combined breadcrumb queue is transient. The idea was to avoid CircularFifoQueue + SynchronizedQueue overhead when only a short-lived merged queue is needed.
+
What changed?
Replace the Scope.createBreadcrumbsList helper call with an ArrayDeque in CombinedScopeView.getBreadcrumbs().
+
Result
Discarded. It saved a small amount of allocation, but CPU and wall time exceeded the 10% guardrail. It also risked subtly changing circular-capacity behavior.
CPU median58.364ms−1.642ms / −2.74% vs baseline; failed guardrail
+
Wall median23.218ms+0.223ms / +0.97% vs baseline; Relay output equivalent
+
+
+
Why try this?
SentryClient.applyScope created new Contexts(scope.getContexts()) just to iterate entries. That deep-cloned context objects before immediately copying selected entries again.
+
What changed?
Iterate scope.getContexts().entrySet() directly in three applyScope variants.
+
Result
Discarded as a standalone attempt. The change was correct, but too small and hit CPU/wall noise guardrails. It was later kept as part of AR-04 with the tags/extras double-copy removal.
+
+
+ Patch-style diff reconstructed from later kept patch
+
diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
+ if (!contexts.containsKey(entry.getKey())) {
+ contexts.put(entry.getKey(), entry.getValue());
+ }
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
+ if (!contexts.containsKey(entry.getKey())) {
+ contexts.put(entry.getKey(), entry.getValue());
+ }
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
+ if (!contexts.containsKey(entry.getKey())) {
+ contexts.put(entry.getKey(), entry.getValue());
+ }
Eliminate applyScope double-copy for tags, extras, and contexts
+
+ kept
+
+
+
Allocation4,024,504B−64,232B / −1.57% vs baseline
+
CPU median48.180ms−11.826ms / −19.71% vs baseline; passed
+
Wall median21.522ms−1.473ms / −6.41% vs baseline; Relay output equivalent
+
+
+
Why try this?
applyScope wrapped scope maps in new HashMap before passing them into setters that already copy internally. Contexts were also deep-copied just to iterate.
+
What changed?
Pass scope maps directly to setters and iterate contexts directly. This keeps the setter-owned copy while removing the redundant outer copy.
+
Result
Kept. Allocation improved by ~17.6KB with CPU/wall inside guardrails.
+
+
+ Patch-style diff actual commit f053b24f50
+
diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java
+@@
+ if (event.getTags() == null) {
+- event.setTags(new HashMap<>(scope.getTags()));
++ event.setTags(scope.getTags());
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
+@@
+ if (replayEvent.getTags() == null) {
+- replayEvent.setTags(new HashMap<>(scope.getTags()));
++ replayEvent.setTags(scope.getTags());
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
+@@
+ if (sentryBaseEvent.getTags() == null) {
+- sentryBaseEvent.setTags(new HashMap<>(scope.getTags()));
++ sentryBaseEvent.setTags(scope.getTags());
+@@
+ if (sentryBaseEvent.getExtras() == null) {
+- sentryBaseEvent.setExtras(new HashMap<>(scope.getExtras()));
++ sentryBaseEvent.setExtras(scope.getExtras());
+@@
+- for (Map.Entry<String, Object> entry : new Contexts(scope.getContexts()).entrySet()) {
++ for (Map.Entry<String, Object> entry : scope.getContexts().entrySet()) {
Allocation3,988,704B−100,032B / −2.45% vs baseline
+
CPU median68.422ms+8.416ms / +14.03% vs baseline; failed guardrail
+
Wall median26.424ms+3.429ms / +14.91% vs baseline; Relay output equivalent
+
+
+
Why try this?
Most benchmark breadcrumbs had no data. Each breadcrumb still eagerly allocated a ConcurrentHashMap for data.
+
What changed?
Initialize data as Collections.emptyMap(), create a HashMap only on first setData(), and use HashMap for copied/deserialized breadcrumb data.
+
Result
Discarded on the first run because CPU/wall exceeded guardrails despite a large allocation win. The same change was retried as AR-10 and kept, showing measurement noise was significant.
+
+
+ Patch-style diff reconstructed from AR-10 kept patch
+
diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java
+@@
+-import java.util.concurrent.ConcurrentHashMap;
++import java.util.HashMap;
+@@
+- private @NotNull Map<String, @NotNull Object> data = new ConcurrentHashMap<>();
++ private @NotNull Map<String, @NotNull Object> data = Collections.emptyMap();
+@@
+- final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
+- if (dataClone != null) {
+- this.data = dataClone;
++ if (!breadcrumb.data.isEmpty()) {
++ this.data = new HashMap<>(breadcrumb.data);
+@@
+- @NotNull Map<String, Object> data = new ConcurrentHashMap<>();
++ @NotNull Map<String, Object> data = new HashMap<>();
+@@
++ if (data.isEmpty() && data == Collections.EMPTY_MAP) {
++ data = new HashMap<>();
++ }
+ data.put(key, value);
+@@
+- CollectionUtils.newConcurrentHashMap(
++ CollectionUtils.newHashMap(
CPU median48.781ms−11.225ms / −18.71% vs baseline; passed but too small / requested discard
+
Wall median20.529ms−2.466ms / −10.72% vs baseline; Relay output equivalent
+
+
+
Why try this?
SpanContext eagerly allocates ConcurrentHashMap instances for tags and data. The hypothesis was that many spans do not write both maps.
+
What changed?
Start tags/data as empty maps and allocate ConcurrentHashMap on first write.
+
Result
Discarded. It saved only ~2.6KB because TransactionContext.fromPropagationContext uses a constructor path that still eagerly creates data. Complexity was not worth the small win.
CPU median48.176ms−11.830ms / −19.71% vs baseline; passed
+
Wall median19.565ms−3.430ms / −14.92% vs baseline; Relay output equivalent
+
+
+
Why try this?
The common workload had breadcrumbs in only one scope. The old code always allocated a list, copied all queues, sorted, created a circular queue, wrapped it, and copied again.
+
What changed?
If exactly one scope has breadcrumbs, return that queue directly. Only do the full merge path when multiple scopes contain breadcrumbs.
+
Result
Kept. Allocation improved by ~84.8KB and wall time improved by ~15% vs baseline.
+
+
+ Patch-style diff actual commit b78d163120
+
diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java
+@@
+ public @NotNull Queue<Breadcrumb> getBreadcrumbs() {
++ final @NotNull Queue<Breadcrumb> globalBc = globalScope.getBreadcrumbs();
++ final @NotNull Queue<Breadcrumb> isolationBc = isolationScope.getBreadcrumbs();
++ final @NotNull Queue<Breadcrumb> currentBc = scope.getBreadcrumbs();
++
++ // Fast path: only one scope has breadcrumbs (common case)
++ final boolean hasGlobal = !globalBc.isEmpty();
++ final boolean hasIsolation = !isolationBc.isEmpty();
++ final boolean hasCurrent = !currentBc.isEmpty();
++
++ if (!hasGlobal && !hasCurrent) {
++ return isolationBc;
++ }
++ if (!hasIsolation && !hasCurrent) {
++ return globalBc;
++ }
++ if (!hasGlobal && !hasIsolation) {
++ return currentBc;
++ }
++
++ // Multiple scopes have breadcrumbs — full merge
+ final @NotNull List<Breadcrumb> allBreadcrumbs = new ArrayList<>();
+- allBreadcrumbs.addAll(globalScope.getBreadcrumbs());
+- allBreadcrumbs.addAll(isolationScope.getBreadcrumbs());
+- allBreadcrumbs.addAll(scope.getBreadcrumbs());
++ allBreadcrumbs.addAll(globalBc);
++ allBreadcrumbs.addAll(isolationBc);
++ allBreadcrumbs.addAll(currentBc);
diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java
+@@
+-import java.util.concurrent.CopyOnWriteArrayList;
+@@
+- final @NotNull List<Attachment> allAttachments = new CopyOnWriteArrayList<>();
++ final @NotNull List<Attachment> allAttachments = new ArrayList<>();
+@@
+- final @NotNull List<EventProcessor> allEventProcessors = new CopyOnWriteArrayList<>();
++ final @NotNull List<EventProcessor> allEventProcessors = new ArrayList<>();
+@@
+- final @NotNull List<EventProcessorAndOrder> allEventProcessors = new CopyOnWriteArrayList<>();
++ final @NotNull List<EventProcessorAndOrder> allEventProcessors = new ArrayList<>();
Allocation3,967,752B−120,984B / −2.96% vs baseline
+
CPU median47.970ms−12.036ms / −20.06% vs baseline; passed
+
Wall median18.248ms−4.747ms / −20.64% vs baseline; Relay output equivalent
+
+
+
Why try this?
AR-06 showed a strong allocation win but failed CPU/wall due to noisy timing. Retrying tested whether the allocation win was real.
+
What changed?
Same lazy Breadcrumb.data idea: default to Collections.emptyMap(), allocate HashMap only on first data write, and avoid ConcurrentHashMap for copy/deserialization paths.
+
Result
Kept. This became the recorded best: 3,967,752B allocated, −120,984B / −2.96% vs baseline. Relay output remained equivalent.
+
+
+ Patch-style diff actual commit 065ea67cfb
+
diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java
+@@
+ import java.io.IOException;
+ import java.util.ArrayList;
++import java.util.HashMap;
+ import java.util.Collections;
+@@
+- private @NotNull Map<String, @NotNull Object> data = new ConcurrentHashMap<>();
++ private @NotNull Map<String, @NotNull Object> data = Collections.emptyMap();
+@@
+- final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
+- if (dataClone != null) {
+- this.data = dataClone;
++ if (!breadcrumb.data.isEmpty()) {
++ this.data = new HashMap<>(breadcrumb.data);
+@@
+- @NotNull Map<String, Object> data = new ConcurrentHashMap<>();
++ @NotNull Map<String, Object> data = new HashMap<>();
+@@
+ } else {
++ if (data.isEmpty() && data == Collections.EMPTY_MAP) {
++ data = new HashMap<>();
++ }
+ data.put(key, value);
+@@
+- if (key == null) {
++ if (key == null || data.isEmpty()) {
+ return;
+@@
+- CollectionUtils.newConcurrentHashMap(
++ CollectionUtils.newHashMap(
Return unmodifiable Scope maps instead of raw internals
+
+ discarded
+
+
+
Allocation3,973,696B−115,040B / −2.81% vs baseline
+
CPU median84.465ms+24.459ms / +40.76% vs baseline; failed guardrail
+
Wall median47.107ms+24.112ms / +104.86% vs baseline; Relay output equivalent
+
+
+
Why try this?
AR-01 returned raw internal maps, which saved copies but exposed mutable Scope internals. This attempted to keep the allocation win without allowing external mutation.
+
What changed?
Return Collections.unmodifiableMap(tags/attributes) from Scope getters.
+
Result
Discarded by measurement and then fully reverted for API safety. The run showed severe CPU/wall regression noise. Final tracked state restored defensive copies for Scope.getTags() and getAttributes().
+
+
+ Patch-style diff actual commit 9af1d54600; later reverted by e65556f610
+
The public Scope getters should not expose mutable internals. After AR-11 was discarded, the safest final code restored the original defensive-copy behavior.
+
What changed?
Reverted Scope.getTags() and getAttributes() to CollectionUtils.newConcurrentHashMap(...).
+
Result
Final worktree no longer contains AR-01’s risky raw-map getter behavior. The remaining final diff contains Breadcrumb.data lazy-init, CombinedScopeView HashMap/ breadcrumb fast path, and SentryClient applyScope copy reductions.
+
+
+ Patch-style diff actual commit e65556f610; corrective change after measured attempts
+
A scrollable summary of the code changes performed and the outcome: matrix jobs now exercise the intended Spring Boot versions, with compatibility fixes for supported sample combinations.
The matrix override did not match the TOML format.
+
The workflows attempted to replace keys like springboot2=..., but the version catalog uses quoted TOML assignments such as springboot2 = "2.7.18". That meant CI could say it was testing a matrix version while Gradle still used the default catalog value.
+
+
BeforeVersion replacement was a no-op
The old sed pattern did not tolerate whitespace or quoted values.
+
FixReplace the quoted TOML value
The new expressions preserve the left-hand assignment and update only the version string.
+
AfterMatrix versions are real
Spring Boot 2.x, 3.x, and 4.x jobs now run with their requested versions.
+
+
+
+
+
+
+
+
Change scope
+
What areas were touched
+
+
CI workflows
Fixed Spring Boot 2/3/4 matrix version substitution.
Trimmed matrix versions to supported combinations.
Set Gradle project flags to exclude GraphQL/Kafka for older Boot 2 jobs.
+
Sample builds
Added optional GraphQL/Kafka dependencies and source exclusions.
Imported the tested Spring Boot BOM in Jakarta samples.
Aligned Java/Kotlin targets to Java 11 for Boot 2 samples.
+
Spring compatibility
Changed GraphQL auto-configuration class checks to string-based names.
Used Spring GraphQL directly as compile-only dependency.
Adjusted batch loader options signature for Spring GraphQL 1.2–1.4 compatibility.
+
System tests
Skipped Kafka broker/profile setup when Kafka is excluded.
Excluded GraphQL/Kafka system test classes for unsupported Boot 2 matrix versions.
Kept backend system test coverage passing for supported modules.
+
+
+
+
+
+
+
+
Key changes
+
Implementation highlights
+
+
Area
Change performed
Outcome
+
Matrix sed
Updated Spring Boot 2/3/4 workflows to replace springbootN = "..." values, preserving whitespace.
The Spring Boot matrix checks completed successfully across supported Boot 2, Boot 3, and Boot 4 versions. Backend system tests for Spring samples also passed with the adjusted dependency/test exclusions.
+
+
5/5Spring Boot 2 matrix jobs passed
2.4.13, 2.5.15, 2.6.15, 2.7.0, and 2.7.18.
+
4/4Spring Boot 3 matrix jobs passed
3.2.12, 3.3.13, 3.4.13, and 3.5.13.
+
2/2Spring Boot 4 matrix jobs passed
4.0.0 and 4.0.5.
+
61successful checks reported by GitHub
Build, format, benchmarks, matrix jobs, backend system tests, Warden, license, secret scan, and size/snapshot checks.
+
1remaining blocking check
The PR is still marked BLOCKED because the aggregate Ui tests check reports failure, even though the listed critical UI test jobs for API 31/33/35/36 show success.
+
+
+
+
+
+
+
+
Verification snapshot
+
CI status at compilation time
+
+
61Success
Completed checks with successful conclusions.
+
1Skipped
validate-high-risk-code was skipped.
+
1Failure
Aggregate Ui tests check reports failure.
+
+
Notable passing checks
+
+
Build Job ubuntu-latest - Java 17
+
Format Code
+
Spring Boot 2.x Matrix: 2.4.13, 2.5.15, 2.6.15, 2.7.0, 2.7.18
+
Spring Boot 3.x Matrix: 3.2.12, 3.3.13, 3.4.13, 3.5.13
+
Spring Boot 4.x Matrix: 4.0.0, 4.0.5
+
System Tests Backend: Spring Boot, WebFlux, Jakarta, OTel, Boot 4, Spring 7, Spring Jakarta, and Spring samples
+
+
+
+
+
+
+
+
+
+
Commit timeline
+
How the fix evolved
+
+
104267513d
Fix Spring Boot matrix version updates
Corrected version catalog replacement so matrix jobs actually test their requested versions.
+
917ebada4d
Limit Spring Boot matrix to supported versions
Removed versions the current sample setup cannot build with.
+
52df6e99f3
Restore Spring Boot matrix coverage
Expanded supported Boot 2/3 matrix coverage and excluded GraphQL where unavailable.
+
b8a9c0bde8
Avoid deprecated Reactor scheduler in sample
Removed scheduler usage that triggered warnings under -Werror.
+
87b8bc3f8a
Exclude Kafka from old Boot 2 matrix
Excluded Kafka paths for old Boot 2 dependency management and aligned test BOMs.
+
37d04c6b59
Support older Spring GraphQL options API
Made the batch loader registry compile across Spring GraphQL 1.2/1.3 and 1.4.
+
+
+
+
+
+
+
+
Current state
+
Ready except for the remaining UI aggregate check.
+
+
Summary
The matrix substitution bug is fixed.
Supported Spring Boot compatibility coverage is restored and passing.
Sample builds/tests now adapt to integrations unavailable in older Boot 2 versions.
PR #5372 remains open and blocked by one reported UI aggregate failure.
+
Notes
This presentation summarizes tracked branch changes compared with main. The working tree also contains an untracked org/datadog/jmxfetch/ directory, which is not part of this PR summary.
Compiled on 2026-05-15 from local git diff and GitHub PR status.