Skip to content

Commit 28a11a7

Browse files
authored
Use Random through ThreadLocal<Random> (#3835)
* Use Random as a ThreadLocal<> * changelog * code review changes
1 parent 5183da9 commit 28a11a7

File tree

7 files changed

+107
-21
lines changed

7 files changed

+107
-21
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Use a separate `Random` instance per thread to improve SDK performance ([#3835](https://github.com/getsentry/sentry-java/pull/3835))
8+
59
### Fixes
610

711
- Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823))

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

+2-14
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
import io.sentry.protocol.SentryTransaction;
5555
import io.sentry.protocol.User;
5656
import io.sentry.util.HintUtils;
57-
import io.sentry.util.Random;
57+
import io.sentry.util.SentryRandom;
5858
import java.io.File;
5959
import java.util.ArrayList;
6060
import java.util.Arrays;
@@ -83,24 +83,13 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor {
8383

8484
private final @NotNull SentryExceptionFactory sentryExceptionFactory;
8585

86-
private final @Nullable Random random;
87-
8886
public AnrV2EventProcessor(
8987
final @NotNull Context context,
9088
final @NotNull SentryAndroidOptions options,
9189
final @NotNull BuildInfoProvider buildInfoProvider) {
92-
this(context, options, buildInfoProvider, null);
93-
}
94-
95-
AnrV2EventProcessor(
96-
final @NotNull Context context,
97-
final @NotNull SentryAndroidOptions options,
98-
final @NotNull BuildInfoProvider buildInfoProvider,
99-
final @Nullable Random random) {
10090
this.context = ContextUtils.getApplicationContext(context);
10191
this.options = options;
10292
this.buildInfoProvider = buildInfoProvider;
103-
this.random = random;
10493

10594
final SentryStackTraceFactory sentryStackTraceFactory =
10695
new SentryStackTraceFactory(this.options);
@@ -180,9 +169,8 @@ private boolean sampleReplay(final @NotNull SentryEvent event) {
180169

181170
try {
182171
// we have to sample here with the old sample rate, because it may change between app launches
183-
final @NotNull Random random = this.random != null ? this.random : new Random();
184172
final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate);
185-
if (replayErrorSampleRateDouble < random.nextDouble()) {
173+
if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) {
186174
options
187175
.getLogger()
188176
.log(

sentry/api/sentry.api

+5
Original file line numberDiff line numberDiff line change
@@ -5829,6 +5829,11 @@ public final class io/sentry/util/SampleRateUtils {
58295829
public static fun isValidTracesSampleRate (Ljava/lang/Double;Z)Z
58305830
}
58315831

5832+
public final class io/sentry/util/SentryRandom {
5833+
public fun <init> ()V
5834+
public static fun current ()Lio/sentry/util/Random;
5835+
}
5836+
58325837
public final class io/sentry/util/StringUtils {
58335838
public static fun byteCountToString (J)Ljava/lang/String;
58345839
public static fun calculateStringHash (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/String;

sentry/src/main/java/io/sentry/SentryClient.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.sentry.util.HintUtils;
1919
import io.sentry.util.Objects;
2020
import io.sentry.util.Random;
21+
import io.sentry.util.SentryRandom;
2122
import io.sentry.util.TracingUtils;
2223
import java.io.Closeable;
2324
import java.io.IOException;
@@ -40,7 +41,6 @@ public final class SentryClient implements ISentryClient, IMetricsClient {
4041

4142
private final @NotNull SentryOptions options;
4243
private final @NotNull ITransport transport;
43-
private final @Nullable Random random;
4444
private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate();
4545
private final @NotNull IMetricsAggregator metricsAggregator;
4646

@@ -66,8 +66,6 @@ public boolean isEnabled() {
6666
options.isEnableMetrics()
6767
? new MetricsAggregator(options, this)
6868
: NoopMetricsAggregator.getInstance();
69-
70-
this.random = options.getSampleRate() == null ? null : new Random();
7169
}
7270

7371
private boolean shouldApplyScopeData(
@@ -1183,6 +1181,7 @@ public boolean isHealthy() {
11831181
}
11841182

11851183
private boolean sample() {
1184+
final @Nullable Random random = options.getSampleRate() == null ? null : SentryRandom.current();
11861185
// https://docs.sentry.io/development/sdk-dev/features/#event-sampling
11871186
if (options.getSampleRate() != null && random != null) {
11881187
final double sampling = options.getSampleRate();

sentry/src/main/java/io/sentry/TracesSampler.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.util.Objects;
44
import io.sentry.util.Random;
5+
import io.sentry.util.SentryRandom;
56
import org.jetbrains.annotations.NotNull;
67
import org.jetbrains.annotations.Nullable;
78
import org.jetbrains.annotations.TestOnly;
@@ -10,14 +11,14 @@ final class TracesSampler {
1011
private static final @NotNull Double DEFAULT_TRACES_SAMPLE_RATE = 1.0;
1112

1213
private final @NotNull SentryOptions options;
13-
private final @NotNull Random random;
14+
private final @Nullable Random random;
1415

1516
public TracesSampler(final @NotNull SentryOptions options) {
16-
this(Objects.requireNonNull(options, "options are required"), new Random());
17+
this(Objects.requireNonNull(options, "options are required"), null);
1718
}
1819

1920
@TestOnly
20-
TracesSampler(final @NotNull SentryOptions options, final @NotNull Random random) {
21+
TracesSampler(final @NotNull SentryOptions options, final @Nullable Random random) {
2122
this.options = options;
2223
this.random = random;
2324
}
@@ -90,6 +91,13 @@ TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) {
9091
}
9192

9293
private boolean sample(final @NotNull Double aDouble) {
93-
return !(aDouble < random.nextDouble());
94+
return !(aDouble < getRandom().nextDouble());
95+
}
96+
97+
private Random getRandom() {
98+
if (random == null) {
99+
return SentryRandom.current();
100+
}
101+
return random;
94102
}
95103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.sentry.util;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
/**
6+
* This SentryRandom is a compromise used for improving performance of the SDK.
7+
*
8+
* <p>We did some testing where using Random from multiple threads degrades performance
9+
* significantly. We opted for this approach as it wasn't easily possible to vendor
10+
* ThreadLocalRandom since it's using advanced features that can cause java.lang.IllegalAccessError.
11+
*/
12+
public final class SentryRandom {
13+
14+
private static final @NotNull SentryRandomThreadLocal instance = new SentryRandomThreadLocal();
15+
16+
/**
17+
* Returns the current threads instance of {@link Random}. An instance of {@link Random} will be
18+
* created the first time this is invoked on each thread.
19+
*
20+
* <p>NOTE: Avoid holding a reference to the returned {@link Random} instance as sharing a
21+
* reference across threads (while being thread-safe) will likely degrade performance
22+
* significantly.
23+
*
24+
* @return random
25+
*/
26+
public static @NotNull Random current() {
27+
return instance.get();
28+
}
29+
30+
private static class SentryRandomThreadLocal extends ThreadLocal<Random> {
31+
32+
@Override
33+
protected Random initialValue() {
34+
return new Random();
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.sentry.util
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertNotSame
5+
import kotlin.test.assertSame
6+
7+
class SentryRandomTest {
8+
9+
@Test
10+
fun `thread local creates a new instance per thread but keeps re-using it for the same thread`() {
11+
val mainThreadRandom1 = SentryRandom.current()
12+
val mainThreadRandom2 = SentryRandom.current()
13+
assertSame(mainThreadRandom1, mainThreadRandom2)
14+
15+
var thread1Random1: Random? = null
16+
var thread1Random2: Random? = null
17+
18+
val thread1 = Thread() {
19+
thread1Random1 = SentryRandom.current()
20+
thread1Random2 = SentryRandom.current()
21+
}
22+
23+
var thread2Random1: Random? = null
24+
var thread2Random2: Random? = null
25+
26+
val thread2 = Thread() {
27+
thread2Random1 = SentryRandom.current()
28+
thread2Random2 = SentryRandom.current()
29+
}
30+
31+
thread1.start()
32+
thread2.start()
33+
thread1.join()
34+
thread2.join()
35+
36+
assertSame(thread1Random1, thread1Random2)
37+
assertNotSame(mainThreadRandom1, thread1Random1)
38+
39+
assertSame(thread2Random1, thread2Random2)
40+
assertNotSame(mainThreadRandom1, thread2Random1)
41+
42+
val mainThreadRandom3 = SentryRandom.current()
43+
assertSame(mainThreadRandom1, mainThreadRandom3)
44+
}
45+
}

0 commit comments

Comments
 (0)