Skip to content

Commit b070303

Browse files
authored
Merge pull request #5598 from getsentry/perf/sdk-overhead-reduction-breadcrumb-lazy-data
perf(core): [SDK Overhead Reduction 7] Lazily allocate Breadcrumb data
2 parents 9766b74 + 5fd3d03 commit b070303

3 files changed

Lines changed: 81 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
### Internal
66

7+
<<<<<<< perf/sdk-overhead-reduction-breadcrumb-lazy-data
8+
- Reduce breadcrumb allocation overhead by creating the `Breadcrumb` data map only when data is added. ([#5598](https://github.com/getsentry/sentry-java/pull/5598))
9+
=======
710
- Reduce JSON serialization overhead by lowering the initial `JsonWriter` nesting stack size while preserving on-demand growth. ([#5591](https://github.com/getsentry/sentry-java/pull/5591))
811
- Reduce timestamp helper overhead by replacing unnecessary `Calendar` usage in `DateUtils` with direct `Date` creation. ([#5589](https://github.com/getsentry/sentry-java/pull/5589))
912
- Reduce Android startup overhead by using the default timezone directly on older devices or when no timezone info is available in the locale. ([#5587](https://github.com/getsentry/sentry-java/pull/5587))
13+
>>>>>>> perf/sdk-overhead-reduction
1014

1115
## 8.45.0
1216

sentry/src/main/java/io/sentry/Breadcrumb.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ public final class Breadcrumb implements JsonUnknown, JsonSerializable, Comparab
3434
/** The type of breadcrumb. */
3535
private @Nullable String type;
3636

37+
private static final @NotNull Map<String, @NotNull Object> EMPTY_DATA = Collections.emptyMap();
38+
3739
/** Data associated with this breadcrumb. */
38-
private @NotNull Map<String, @NotNull Object> data = new ConcurrentHashMap<>();
40+
private volatile @NotNull Map<String, @NotNull Object> data = EMPTY_DATA;
3941

4042
/** Dotted strings that indicate what the crumb is or where it comes from. */
4143
private @Nullable String category;
@@ -78,9 +80,11 @@ public Breadcrumb(final long timestamp) {
7880
this.type = breadcrumb.type;
7981
this.category = breadcrumb.category;
8082
this.origin = breadcrumb.origin;
81-
final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
82-
if (dataClone != null) {
83-
this.data = dataClone;
83+
if (!breadcrumb.data.isEmpty()) {
84+
final Map<String, Object> dataClone = CollectionUtils.newConcurrentHashMap(breadcrumb.data);
85+
if (dataClone != null) {
86+
this.data = dataClone;
87+
}
8488
}
8589
this.unknown = CollectionUtils.newConcurrentHashMap(breadcrumb.unknown);
8690
this.level = breadcrumb.level;
@@ -100,7 +104,7 @@ public static Breadcrumb fromMap(
100104
@NotNull Date timestamp = DateUtils.getCurrentDateTime();
101105
String message = null;
102106
String type = null;
103-
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
107+
Map<String, Object> data = null;
104108
String category = null;
105109
String origin = null;
106110
SentryLevel level = null;
@@ -129,6 +133,9 @@ public static Breadcrumb fromMap(
129133
if (untypedData != null) {
130134
for (Map.Entry<Object, Object> dataEntry : untypedData.entrySet()) {
131135
if (dataEntry.getKey() instanceof String && dataEntry.getValue() != null) {
136+
if (data == null) {
137+
data = new ConcurrentHashMap<>();
138+
}
132139
data.put((String) dataEntry.getKey(), dataEntry.getValue());
133140
} else {
134141
options
@@ -166,7 +173,9 @@ public static Breadcrumb fromMap(
166173
final Breadcrumb breadcrumb = new Breadcrumb(timestamp);
167174
breadcrumb.message = message;
168175
breadcrumb.type = type;
169-
breadcrumb.data = data;
176+
if (data != null) {
177+
breadcrumb.data = data;
178+
}
170179
breadcrumb.category = category;
171180
breadcrumb.origin = origin;
172181
breadcrumb.level = level;
@@ -494,7 +503,7 @@ public static Breadcrumb fromMap(
494503
breadcrumb.setData("view.tag", viewTag);
495504
}
496505
for (final Map.Entry<String, Object> entry : additionalData.entrySet()) {
497-
breadcrumb.getData().put(entry.getKey(), entry.getValue());
506+
breadcrumb.setData(entry.getKey(), entry.getValue());
498507
}
499508
breadcrumb.setLevel(SentryLevel.INFO);
500509
return breadcrumb;
@@ -598,6 +607,20 @@ public void setType(@Nullable String type) {
598607
this.type = type;
599608
}
600609

610+
private @NotNull Map<String, @NotNull Object> getOrCreateData() {
611+
Map<String, @NotNull Object> currentData = data;
612+
if (currentData == EMPTY_DATA) {
613+
synchronized (this) {
614+
currentData = data;
615+
if (currentData == EMPTY_DATA) {
616+
currentData = new ConcurrentHashMap<>();
617+
data = currentData;
618+
}
619+
}
620+
}
621+
return currentData;
622+
}
623+
601624
/**
602625
* Returns the data map
603626
*
@@ -606,7 +629,7 @@ public void setType(@Nullable String type) {
606629
@ApiStatus.Internal
607630
@NotNull
608631
public Map<String, Object> getData() {
609-
return data;
632+
return getOrCreateData();
610633
}
611634

612635
/**
@@ -636,7 +659,7 @@ public void setData(@Nullable String key, @Nullable Object value) {
636659
if (value == null) {
637660
removeData(key);
638661
} else {
639-
data.put(key, value);
662+
getOrCreateData().put(key, value);
640663
}
641664
}
642665

@@ -649,7 +672,10 @@ public void removeData(@Nullable String key) {
649672
if (key == null) {
650673
return;
651674
}
652-
data.remove(key);
675+
final Map<String, @NotNull Object> currentData = data;
676+
if (currentData != EMPTY_DATA) {
677+
currentData.remove(key);
678+
}
653679
}
654680

655681
/**
@@ -859,7 +885,7 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
859885
@NotNull Date timestamp = DateUtils.getCurrentDateTime();
860886
String message = null;
861887
String type = null;
862-
@NotNull Map<String, Object> data = new ConcurrentHashMap<>();
888+
Map<String, Object> data = null;
863889
String category = null;
864890
String origin = null;
865891
SentryLevel level = null;
@@ -884,7 +910,7 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
884910
Map<String, Object> deserializedData =
885911
CollectionUtils.newConcurrentHashMap(
886912
(Map<String, Object>) reader.nextObjectOrNull());
887-
if (deserializedData != null) {
913+
if (deserializedData != null && !deserializedData.isEmpty()) {
888914
data = deserializedData;
889915
}
890916
break;
@@ -913,7 +939,9 @@ public static final class Deserializer implements JsonDeserializer<Breadcrumb> {
913939
Breadcrumb breadcrumb = new Breadcrumb(timestamp);
914940
breadcrumb.message = message;
915941
breadcrumb.type = type;
916-
breadcrumb.data = data;
942+
if (data != null) {
943+
breadcrumb.data = data;
944+
}
917945
breadcrumb.category = category;
918946
breadcrumb.origin = origin;
919947
breadcrumb.level = level;

sentry/src/test/java/io/sentry/BreadcrumbTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.sentry
22

33
import java.util.Date
4+
import java.util.concurrent.CountDownLatch
5+
import java.util.concurrent.Executors
6+
import java.util.concurrent.TimeUnit
47
import kotlin.test.Test
58
import kotlin.test.assertEquals
69
import kotlin.test.assertFalse
@@ -329,6 +332,39 @@ class BreadcrumbTest {
329332
breadcrumb.removeData(null)
330333
}
331334

335+
@Test
336+
fun `getData returns mutable map for new breadcrumb`() {
337+
val breadcrumb = Breadcrumb()
338+
339+
breadcrumb.data["k"] = "v"
340+
341+
assertEquals("v", breadcrumb.getData("k"))
342+
}
343+
344+
@Test
345+
fun `concurrent first writes keep all data entries`() {
346+
val breadcrumb = Breadcrumb()
347+
val count = 32
348+
val executor = Executors.newFixedThreadPool(count)
349+
val start = CountDownLatch(1)
350+
val futures =
351+
(0 until count).map { index ->
352+
executor.submit {
353+
start.await()
354+
breadcrumb.setData("key-$index", index)
355+
}
356+
}
357+
358+
start.countDown()
359+
futures.forEach { it.get(5, TimeUnit.SECONDS) }
360+
executor.shutdown()
361+
362+
assertEquals(count, breadcrumb.data.size)
363+
for (index in 0 until count) {
364+
assertEquals(index, breadcrumb.data["key-$index"])
365+
}
366+
}
367+
332368
class TestKey(val id: Long) {
333369
override fun toString(): String = id.toString()
334370
}

0 commit comments

Comments
 (0)