Skip to content

Commit 05d60fb

Browse files
authored
Merge branch 'perf/sdk-overhead-reduction' into perf/sdk-overhead-reduction-reflection-visiting
2 parents f013263 + f0276a6 commit 05d60fb

13 files changed

Lines changed: 175 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
### Internal
66

77
- Reduce JSON serialization overhead by allocating reflection cycle-tracking state only when reflection serialization is used. ([#5600](https://github.com/getsentry/sentry-java/pull/5600))
8+
- Reduce context serialization overhead by sorting key snapshots with arrays instead of temporary lists. ([#5599](https://github.com/getsentry/sentry-java/pull/5599))
9+
- Reduce breadcrumb allocation overhead by creating the `Breadcrumb` data map only when data is added. ([#5598](https://github.com/getsentry/sentry-java/pull/5598))
10+
- 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))
11+
- Reduce timestamp helper overhead by replacing unnecessary `Calendar` usage in `DateUtils` with direct `Date` creation. ([#5589](https://github.com/getsentry/sentry-java/pull/5589))
12+
- 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))
813

914
## 8.45.0
1015

sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,19 @@ private void setDeviceIO(
257257
@SuppressWarnings("NewApi")
258258
@NotNull
259259
private TimeZone getTimeZone() {
260-
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) {
260+
// Only use the costly Calendar API on Android 13+ (API Level 33+) when the locale contains a
261+
// Unicode timezone extension (for example "en-US-u-tz-usnyc"), because Calendar honors that
262+
// extension. For all other cases, use the process default timezone directly for performance.
263+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
261264
LocaleList locales = context.getResources().getConfiguration().getLocales();
262265
if (!locales.isEmpty()) {
263266
Locale locale = locales.get(0);
264-
return Calendar.getInstance(locale).getTimeZone();
267+
if (locale.getUnicodeLocaleType("tz") != null) {
268+
return Calendar.getInstance(locale).getTimeZone();
269+
}
265270
}
266271
}
267-
return Calendar.getInstance().getTimeZone();
272+
return TimeZone.getDefault();
268273
}
269274

270275
@SuppressWarnings("JdkObsolete")

sentry-android-core/src/test/java/io/sentry/android/core/DeviceInfoUtilTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@ package io.sentry.android.core
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.content.res.Configuration
56
import android.os.BatteryManager
7+
import android.os.Build
8+
import android.os.LocaleList
69
import androidx.test.core.app.ApplicationProvider
710
import androidx.test.ext.junit.runners.AndroidJUnit4
811
import io.sentry.android.core.internal.util.CpuInfoUtils
12+
import java.util.Locale
13+
import java.util.TimeZone
914
import kotlin.test.BeforeTest
1015
import kotlin.test.Test
1116
import kotlin.test.assertEquals
1217
import kotlin.test.assertNotNull
1318
import kotlin.test.assertNull
1419
import org.junit.runner.RunWith
20+
import org.robolectric.annotation.Config
1521

1622
@RunWith(AndroidJUnit4::class)
1723
class DeviceInfoUtilTest {
@@ -47,6 +53,32 @@ class DeviceInfoUtilTest {
4753
assertNotNull(deviceInfo.memorySize)
4854
}
4955

56+
@Test
57+
fun `sets default timezone`() {
58+
val deviceInfoUtil = DeviceInfoUtil.getInstance(context, SentryAndroidOptions())
59+
val deviceInfo = deviceInfoUtil.collectDeviceInformation(false, false)
60+
61+
assertEquals(TimeZone.getDefault(), deviceInfo.timezone)
62+
}
63+
64+
@Test
65+
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
66+
fun `preserves timezone from locale unicode extension`() {
67+
val defaultTimeZone = TimeZone.getDefault()
68+
try {
69+
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
70+
val configuration = Configuration(context.resources.configuration)
71+
configuration.setLocales(LocaleList(Locale.forLanguageTag("en-US-u-tz-usnyc")))
72+
val localizedContext = context.createConfigurationContext(configuration)
73+
val deviceInfoUtil = DeviceInfoUtil(localizedContext, SentryAndroidOptions())
74+
val deviceInfo = deviceInfoUtil.collectDeviceInformation(false, false)
75+
76+
assertEquals("America/New_York", deviceInfo.timezone?.id)
77+
} finally {
78+
TimeZone.setDefault(defaultTimeZone)
79+
}
80+
}
81+
5082
@Test
5183
fun `does include cpu data`() {
5284
CpuInfoUtils.getInstance().setCpuMaxFrequencies(listOf(1024))

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7619,6 +7619,7 @@ public final class io/sentry/util/CollectionUtils {
76197619
public static fun newHashMap (Ljava/util/Map;)Ljava/util/Map;
76207620
public static fun reverseListIterator (Ljava/util/concurrent/CopyOnWriteArrayList;)Ljava/util/ListIterator;
76217621
public static fun size (Ljava/lang/Iterable;)I
7622+
public static fun toSortedStringArray (Ljava/util/Enumeration;I)[Ljava/lang/String;
76227623
}
76237624

76247625
public abstract interface class io/sentry/util/CollectionUtils$Mapper {

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

Lines changed: 42 additions & 14 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;
@@ -555,7 +564,7 @@ public Breadcrumb(@Nullable String message) {
555564
if (timestamp != null) {
556565
return (Date) timestamp.clone();
557566
} else if (timestampMs != null) {
558-
// we memoize it here into timestamp to avoid instantiating Calendar again and again
567+
// we memoize it here into timestamp to avoid creating a Date again and again
559568
timestamp = DateUtils.getDateTime(timestampMs);
560569
return timestamp;
561570
}
@@ -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/main/java/io/sentry/DateUtils.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package io.sentry;
22

3-
import static io.sentry.vendor.gson.internal.bind.util.ISO8601Utils.TIMEZONE_UTC;
4-
53
import io.sentry.vendor.gson.internal.bind.util.ISO8601Utils;
64
import java.math.BigDecimal;
75
import java.math.RoundingMode;
86
import java.text.ParseException;
97
import java.text.ParsePosition;
10-
import java.util.Calendar;
118
import java.util.Date;
129
import org.jetbrains.annotations.ApiStatus;
1310
import org.jetbrains.annotations.NotNull;
@@ -24,10 +21,9 @@ private DateUtils() {}
2421
*
2522
* @return the UTC Date
2623
*/
27-
@SuppressWarnings("JdkObsolete")
24+
@SuppressWarnings("JavaUtilDate")
2825
public static @NotNull Date getCurrentDateTime() {
29-
final Calendar calendar = Calendar.getInstance(TIMEZONE_UTC);
30-
return calendar.getTime();
26+
return new Date();
3127
}
3228

3329
/**
@@ -78,10 +74,9 @@ private DateUtils() {}
7874
* @param millis the UTC millis from the epoch
7975
* @return the UTC Date
8076
*/
77+
@SuppressWarnings("JavaUtilDate")
8178
public static @NotNull Date getDateTime(final long millis) {
82-
final Calendar calendar = Calendar.getInstance(TIMEZONE_UTC);
83-
calendar.setTimeInMillis(millis);
84-
return calendar.getTime();
79+
return new Date(millis);
8580
}
8681

8782
/**

sentry/src/main/java/io/sentry/MonitorContexts.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package io.sentry;
22

3+
import io.sentry.util.CollectionUtils;
34
import io.sentry.util.Objects;
45
import io.sentry.vendor.gson.stream.JsonToken;
56
import java.io.IOException;
6-
import java.util.Collections;
7-
import java.util.List;
87
import java.util.concurrent.ConcurrentHashMap;
98
import org.jetbrains.annotations.NotNull;
109
import org.jetbrains.annotations.Nullable;
@@ -49,8 +48,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
4948
throws IOException {
5049
writer.beginObject();
5150
// Serialize in alphabetical order to keep determinism.
52-
final List<String> sortedKeys = Collections.list(keys());
53-
Collections.sort(sortedKeys);
51+
final String[] sortedKeys = CollectionUtils.toSortedStringArray(keys(), size());
5452
for (final String key : sortedKeys) {
5553
final Object value = get(key);
5654
if (value != null) {

sentry/src/main/java/io/sentry/protocol/Contexts.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
import io.sentry.ProfileContext;
1111
import io.sentry.SpanContext;
1212
import io.sentry.util.AutoClosableReentrantLock;
13+
import io.sentry.util.CollectionUtils;
1314
import io.sentry.util.HintUtils;
1415
import io.sentry.util.Objects;
1516
import io.sentry.vendor.gson.stream.JsonToken;
1617
import java.io.IOException;
17-
import java.util.Collections;
1818
import java.util.Enumeration;
1919
import java.util.HashMap;
20-
import java.util.List;
2120
import java.util.Map;
2221
import java.util.Set;
2322
import java.util.concurrent.ConcurrentHashMap;
@@ -302,8 +301,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
302301
throws IOException {
303302
writer.beginObject();
304303
// Serialize in alphabetical order to keep determinism.
305-
final List<String> sortedKeys = Collections.list(keys());
306-
Collections.sort(sortedKeys);
304+
final String[] sortedKeys = CollectionUtils.toSortedStringArray(keys(), internalStorage.size());
307305
for (final String key : sortedKeys) {
308306
final Object value = get(key);
309307
if (value != null) {

sentry/src/main/java/io/sentry/util/CollectionUtils.java

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

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.Collection;
6+
import java.util.Enumeration;
57
import java.util.HashMap;
68
import java.util.List;
79
import java.util.ListIterator;
@@ -15,9 +17,28 @@
1517
/** Util class for Collections */
1618
@ApiStatus.Internal
1719
public final class CollectionUtils {
20+
private static final String[] EMPTY_STRINGS = new String[0];
1821

1922
private CollectionUtils() {}
2023

24+
public static @NotNull String[] toSortedStringArray(
25+
final @NotNull Enumeration<String> source, final int size) {
26+
String[] sorted = size == 0 ? EMPTY_STRINGS : new String[size];
27+
int index = 0;
28+
while (source.hasMoreElements()) {
29+
if (index == sorted.length) {
30+
sorted = Arrays.copyOf(sorted, sorted.length + 1);
31+
}
32+
sorted[index] = source.nextElement();
33+
index++;
34+
}
35+
if (index != sorted.length) {
36+
sorted = Arrays.copyOf(sorted, index);
37+
}
38+
Arrays.sort(sorted);
39+
return sorted;
40+
}
41+
2142
/**
2243
* Returns an Iterator size
2344
*

sentry/src/main/java/io/sentry/vendor/gson/stream/JsonWriter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
// Source: https://github.com/google/gson
1818
// Tag: gson-parent-2.8.7
1919
// Commit Hash: 4520489c29e770c64b11ca35e0a0fdf17a1874ab
20-
// Changes: @ApiStatus.Internal, SuppressWarnings
20+
// Changes: @ApiStatus.Internal, SuppressWarnings, reduced stack size
2121

2222
package io.sentry.vendor.gson.stream;
2323

@@ -175,7 +175,7 @@ public class JsonWriter implements Closeable, Flushable {
175175
/** The output data, containing at most one top-level array or object. */
176176
private final Writer out;
177177

178-
private int[] stack = new int[32];
178+
private int[] stack = new int[8];
179179
private int stackSize = 0;
180180
{
181181
push(EMPTY_DOCUMENT);

0 commit comments

Comments
 (0)