Skip to content

Commit 9fba6e3

Browse files
lcianadinauer
andauthored
Fix misuses of CopyOnWriteArrayList (#4247)
* Avoid copying and iterate correctly on `SentryTracer.children` * another place * changelog * fix * remove unnecessary test * wip * improve * improve * Update CHANGELOG.md Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com> --------- Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com>
1 parent 0b8cee0 commit 9fba6e3

File tree

5 files changed

+65
-16
lines changed

5 files changed

+65
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
- Pass OpenTelemetry span attributes into TracesSampler callback ([#4253](https://github.com/getsentry/sentry-java/pull/4253))
2020
- `SamplingContext` now has a `getAttribute` method that grants access to OpenTelemetry span attributes via their String key (e.g. `http.request.method`)
2121
- Fix AbstractMethodError when using SentryTraced for Jetpack Compose ([#4255](https://github.com/getsentry/sentry-java/pull/4255))
22+
- Avoid unnecessary copies when using `CopyOnWriteArrayList` ([#4247](https://github.com/getsentry/sentry-java/pull/4247))
23+
- This affects in particular `SentryTracer.getLatestActiveSpan` which would have previously copied all child span references. This may have caused `OutOfMemoryError` on certain devices due to high frequency of calling the method.
2224

2325
### Features
2426

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6095,6 +6095,7 @@ public final class io/sentry/util/CollectionUtils {
60956095
public static fun newArrayList (Ljava/util/List;)Ljava/util/List;
60966096
public static fun newConcurrentHashMap (Ljava/util/Map;)Ljava/util/Map;
60976097
public static fun newHashMap (Ljava/util/Map;)Ljava/util/Map;
6098+
public static fun reverseListIterator (Ljava/util/concurrent/CopyOnWriteArrayList;)Ljava/util/ListIterator;
60986099
public static fun size (Ljava/lang/Iterable;)I
60996100
}
61006101

sentry/src/main/java/io/sentry/SentryTracer.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import io.sentry.protocol.SentryTransaction;
66
import io.sentry.protocol.TransactionNameSource;
77
import io.sentry.util.AutoClosableReentrantLock;
8+
import io.sentry.util.CollectionUtils;
89
import io.sentry.util.Objects;
910
import io.sentry.util.SpanUtils;
10-
import java.util.ArrayList;
1111
import java.util.List;
1212
import java.util.ListIterator;
1313
import java.util.Map;
@@ -155,7 +155,9 @@ private void onDeadlineTimeoutReached() {
155155
// abort all child-spans first, this ensures the transaction can be finished,
156156
// even if waitForChildren is true
157157
// iterate in reverse order to ensure leaf spans are processed before their parents
158-
@NotNull final ListIterator<Span> iterator = children.listIterator(children.size());
158+
@NotNull
159+
final ListIterator<Span> iterator =
160+
CollectionUtils.reverseListIterator((CopyOnWriteArrayList<Span>) this.children);
159161
while (iterator.hasPrevious()) {
160162
@NotNull final Span span = iterator.previous();
161163
span.setSpanFinishedCallback(null);
@@ -677,14 +679,13 @@ private void updateBaggageValues(final @NotNull Baggage baggage) {
677679
}
678680

679681
private boolean hasAllChildrenFinished() {
680-
final List<Span> spans = new ArrayList<>(this.children);
681-
if (!spans.isEmpty()) {
682-
for (final Span span : spans) {
683-
// This is used in the spanFinishCallback, when the span isn't finished, but has a finish
684-
// date
685-
if (!span.isFinished() && span.getFinishDate() == null) {
686-
return false;
687-
}
682+
@NotNull final ListIterator<Span> iterator = this.children.listIterator();
683+
while (iterator.hasNext()) {
684+
@NotNull final Span span = iterator.next();
685+
// This is used in the spanFinishCallback, when the span isn't finished, but has a finish
686+
// date
687+
if (!span.isFinished() && span.getFinishDate() == null) {
688+
return false;
688689
}
689690
}
690691
return true;
@@ -909,12 +910,13 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac
909910

910911
@Override
911912
public @Nullable ISpan getLatestActiveSpan() {
912-
final List<Span> spans = new ArrayList<>(this.children);
913-
if (!spans.isEmpty()) {
914-
for (int i = spans.size() - 1; i >= 0; i--) {
915-
if (!spans.get(i).isFinished()) {
916-
return spans.get(i);
917-
}
913+
@NotNull
914+
final ListIterator<Span> iterator =
915+
CollectionUtils.reverseListIterator((CopyOnWriteArrayList<Span>) this.children);
916+
while (iterator.hasPrevious()) {
917+
@NotNull final Span span = iterator.previous();
918+
if (!span.isFinished()) {
919+
return span;
918920
}
919921
}
920922
return null;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import java.util.Collection;
55
import java.util.HashMap;
66
import java.util.List;
7+
import java.util.ListIterator;
78
import java.util.Map;
89
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.concurrent.CopyOnWriteArrayList;
911
import org.jetbrains.annotations.ApiStatus;
1012
import org.jetbrains.annotations.NotNull;
1113
import org.jetbrains.annotations.Nullable;
@@ -179,4 +181,23 @@ public interface Predicate<T> {
179181
public interface Mapper<T, R> {
180182
R map(T t);
181183
}
184+
185+
/**
186+
* Returns a reverse iterator, where the first (resp. last) valid call to `prev` returns the last
187+
* (resp. first) element that would be returned when iterating forwards. Note that this differs
188+
* from the behavior of e.g. `org.apache.commons.collections4.iterators.ReverseListIterator`,
189+
* where you need to iterate using `next` instead. We use the concrete type `CopyOnWriteArrayList`
190+
* here as we are relying on the fact that its copy constructor only copies the reference to an
191+
* internal array. We don't want to use this for other `List` implementations, as it could lead to
192+
* an unnecessary copy of the elements instead.
193+
*
194+
* @param list the `CopyOnWriteArrayList` to get the reverse iterator for
195+
* @param <T> the type
196+
* @return a reverse iterator over `list`
197+
*/
198+
public static @NotNull <T> ListIterator<T> reverseListIterator(
199+
final @NotNull CopyOnWriteArrayList<T> list) {
200+
final @NotNull CopyOnWriteArrayList<T> copy = new CopyOnWriteArrayList<>(list);
201+
return copy.listIterator(copy.size());
202+
}
182203
}

sentry/src/test/java/io/sentry/util/CollectionUtilsTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.sentry.util
22

33
import io.sentry.JsonObjectReader
44
import java.io.StringReader
5+
import java.util.concurrent.CopyOnWriteArrayList
56
import kotlin.test.Test
67
import kotlin.test.assertEquals
78
import kotlin.test.assertFalse
@@ -79,4 +80,26 @@ class CollectionUtilsTest {
7980
fun `contains returns false if element is not present`() {
8081
assertFalse(CollectionUtils.contains(arrayOf("one", "two", "three"), "four"))
8182
}
83+
84+
@Test
85+
fun `reverseListIterator returns empty iterator if list is empty`() {
86+
val list = CopyOnWriteArrayList<String>()
87+
val iterator = CollectionUtils.reverseListIterator(list)
88+
assertFalse(iterator.hasNext())
89+
assertFalse(iterator.hasPrevious())
90+
}
91+
92+
@Test
93+
fun `reverseListIterator returns reversed iterator if list is not empty`() {
94+
val elements = listOf("one", "two", "three")
95+
val list = CopyOnWriteArrayList(elements)
96+
val iterator = CollectionUtils.reverseListIterator(list)
97+
assertFalse(iterator.hasNext())
98+
assertTrue(iterator.hasPrevious())
99+
val reversedElements = mutableListOf<String>()
100+
while (iterator.hasPrevious()) {
101+
reversedElements.add(iterator.previous())
102+
}
103+
assertEquals(elements.reversed(), reversedElements)
104+
}
82105
}

0 commit comments

Comments
 (0)