Skip to content

Commit 18379f3

Browse files
authored
Refactor and add tests for optimized assembly traceback (#3949)
Follow-up to #3901: * Renamed `Substring` to `StackLineView` * Implemented tests for `StackLineView` * Corrected `contains` and `startsWith` implementations
1 parent 0a87988 commit 18379f3

File tree

2 files changed

+114
-32
lines changed

2 files changed

+114
-32
lines changed

reactor-core/src/main/java/reactor/core/publisher/Traces.java

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,14 @@ static boolean isUserCode(String line) {
120120
* from the assembly stack trace
121121
*/
122122
static String[] extractOperatorAssemblyInformationParts(String source) {
123-
Iterator<Substring> traces = trimmedNonemptyLines(source);
123+
Iterator<StackLineView> traces = trimmedNonemptyLines(source);
124124

125125
if (!traces.hasNext()) {
126126
return new String[0];
127127
}
128128

129-
Substring prevLine = null;
130-
Substring currentLine = traces.next();
129+
StackLineView prevLine = null;
130+
StackLineView currentLine = traces.next();
131131

132132
if (currentLine.isUserCode()) {
133133
// No line is a Reactor API line.
@@ -157,20 +157,20 @@ static String[] extractOperatorAssemblyInformationParts(String source) {
157157
*
158158
* @implNote This implementation attempts to minimize allocations.
159159
*/
160-
private static Iterator<Substring> trimmedNonemptyLines(String source) {
161-
return new Iterator<Substring>() {
162-
private int index = 0;
160+
private static Iterator<StackLineView> trimmedNonemptyLines(String source) {
161+
return new Iterator<StackLineView>() {
162+
private int index = 0;
163163
@Nullable
164-
private Substring next = getNextLine();
164+
private StackLineView next = getNextLine();
165165

166166
@Override
167167
public boolean hasNext() {
168168
return next != null;
169169
}
170170

171171
@Override
172-
public Substring next() {
173-
Substring current = next;
172+
public StackLineView next() {
173+
StackLineView current = next;
174174
if (current == null) {
175175
throw new NoSuchElementException();
176176
}
@@ -179,13 +179,13 @@ public Substring next() {
179179
}
180180

181181
@Nullable
182-
private Substring getNextLine() {
182+
private StackLineView getNextLine() {
183183
while (index < source.length()) {
184184
int end = source.indexOf('\n', index);
185185
if (end == -1) {
186186
end = source.length();
187187
}
188-
Substring line = new Substring(source, index, end).trim();
188+
StackLineView line = new StackLineView(source, index, end).trim();
189189
index = end + 1;
190190
if (!line.isEmpty()) {
191191
return line;
@@ -196,63 +196,70 @@ private Substring getNextLine() {
196196
};
197197
}
198198

199-
// XXX: Explain.
200-
private static final class Substring {
201-
private final String str;
202-
private final int start;
203-
private final int end;
199+
/**
200+
* Provides optimized access to underlying {@link String} with common operations to
201+
* view the stack trace line without unnecessary allocation of temporary
202+
* {@code String} objects.
203+
*/
204+
static final class StackLineView {
205+
206+
private final String underlying;
207+
private final int start;
208+
private final int end;
204209

205-
Substring(String str, int start, int end) {
206-
this.str = str;
210+
StackLineView(String underlying, int start, int end) {
211+
this.underlying = underlying;
207212
this.start = start;
208213
this.end = end;
209214
}
210215

211-
Substring trim() {
216+
StackLineView trim() {
212217
int newStart = start;
213-
while (newStart < end && str.charAt(newStart) <= ' ') {
218+
while (newStart < end && underlying.charAt(newStart) <= ' ') {
214219
newStart++;
215220
}
216221
int newEnd = end;
217-
while (newEnd > newStart && str.charAt(newEnd - 1) <= ' ') {
222+
while (newEnd > newStart && underlying.charAt(newEnd - 1) <= ' ') {
218223
newEnd--;
219224
}
220-
return newStart == start && newEnd == end ? this : new Substring(str, newStart, newEnd);
225+
return newStart == start && newEnd == end ? this : new StackLineView(
226+
underlying, newStart, newEnd);
221227
}
222228

223229
boolean isEmpty() {
224230
return start == end;
225231
}
226232

227233
boolean startsWith(String prefix) {
228-
return str.startsWith(prefix, start);
234+
boolean canFit = end - start >= prefix.length();
235+
return canFit && underlying.startsWith(prefix, start);
229236
}
230237

231238
boolean contains(String substring) {
232-
int index = str.indexOf(substring, start);
233-
return index >= 0 && index < end;
239+
int index = underlying.indexOf(substring, start);
240+
return index >= start && (index + (substring.length() - 1) < end);
234241
}
235242

236243
boolean isUserCode() {
237244
return !startsWith(PUBLISHER_PACKAGE_PREFIX) || contains("Test");
238245
}
239246

240-
Substring withoutLocationSuffix() {
241-
int linePartIndex = str.indexOf('(', start);
247+
StackLineView withoutLocationSuffix() {
248+
int linePartIndex = underlying.indexOf('(', start);
242249
return linePartIndex > 0 && linePartIndex < end
243-
? new Substring(str, start, linePartIndex)
250+
? new StackLineView(underlying, start, linePartIndex)
244251
: this;
245252
}
246253

247-
Substring withoutPublisherPackagePrefix() {
254+
StackLineView withoutPublisherPackagePrefix() {
248255
return startsWith(PUBLISHER_PACKAGE_PREFIX)
249-
? new Substring(str, start + PUBLISHER_PACKAGE_PREFIX.length(), end)
256+
? new StackLineView(underlying, start + PUBLISHER_PACKAGE_PREFIX.length(), end)
250257
: this;
251258
}
252259

253260
@Override
254261
public String toString() {
255-
return str.substring(start, end);
262+
return underlying.substring(start, end);
256263
}
257264
}
258265
}

reactor-core/src/test/java/reactor/core/publisher/TracesTest.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved.
2+
* Copyright (c) 2018-2024 VMware Inc. or its affiliates, All Rights Reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -146,4 +146,79 @@ public void shouldSanitizeTrue() {
146146
assertThat(Traces.shouldSanitize("java.lang.reflect")).isTrue();
147147
}
148148

149+
@Test
150+
void stackLineViewSanityTest() {
151+
String stackLine = "\treactor.core.publisher.Flux.filter(Flux.java:4209)\n";
152+
153+
int end = stackLine.indexOf('\n');
154+
if (end == -1) {
155+
System.out.println("No end-of-line");
156+
end = stackLine.length();
157+
}
158+
159+
Traces.StackLineView view = new Traces.StackLineView(stackLine, 0, end)
160+
.trim();
161+
162+
assertThat(view.toString()).isEqualTo(stackLine.trim());
163+
assertThat(view.isEmpty()).isFalse();
164+
assertThat(view.isUserCode()).isFalse();
165+
assertThat(view.contains("Flux.filter")).isTrue();
166+
assertThat(view.startsWith("reactor.core.publisher.Flux")).isTrue();
167+
}
168+
169+
@Test
170+
void stackLineViewLimitsAreCheckedAtStart() {
171+
String stackLine = "\treactor.core.publisher.Flux.filter(Flux.java:4209)\n";
172+
173+
Traces.StackLineView incompleteView =
174+
new Traces.StackLineView(stackLine, stackLine.length() / 2, stackLine.length())
175+
.trim();
176+
177+
assertThat(incompleteView.toString()).isEqualTo("ux.filter(Flux.java:4209)");
178+
assertThat(incompleteView.contains(".filter")).isTrue();
179+
assertThat(incompleteView.contains("ux.f")).isTrue();
180+
assertThat(incompleteView.contains("lux.f")).isFalse();
181+
assertThat(incompleteView.contains("09)")).isTrue();
182+
assertThat(incompleteView.startsWith("ux.")).isTrue();
183+
assertThat(incompleteView.startsWith("lux.")).isFalse();
184+
assertThat(incompleteView.startsWith("ux.filter(Flux.java:4209)")).isTrue();
185+
assertThat(incompleteView.startsWith("lux.filter(Flux.java:4209)")).isFalse();
186+
}
187+
188+
@Test
189+
void stackLineViewLimitsAreCheckedAtEnd() {
190+
String stackLine = "\treactor.core.publisher.Flux.filter(Flux.java:4209)\n";
191+
192+
Traces.StackLineView incompleteView =
193+
new Traces.StackLineView(stackLine, 0, stackLine.length() / 2)
194+
.trim();
195+
196+
assertThat(incompleteView.toString()).isEqualTo("reactor.core.publisher.Fl");
197+
assertThat(incompleteView.contains("Fl")).isTrue();
198+
assertThat(incompleteView.contains("Flu")).isFalse();
199+
assertThat(incompleteView.startsWith("reactor.core.publisher.Fl")).isTrue();
200+
assertThat(incompleteView.startsWith("reactor.core.publisher.Flux")).isFalse();
201+
}
202+
203+
@Test
204+
void stackLineViewLocationSuffixGetsRemoved() {
205+
String stackLine = "\treactor.core.publisher.Flux.filter(Flux.java:4209)\n";
206+
207+
Traces.StackLineView view =
208+
new Traces.StackLineView(stackLine, 0, stackLine.length()).trim();
209+
210+
assertThat(view.withoutLocationSuffix()
211+
.toString()).isEqualTo("reactor.core.publisher.Flux.filter");
212+
}
213+
214+
@Test
215+
void stackLineViewPublisherPackagePrefixGetsRemoved() {
216+
String stackLine = "\treactor.core.publisher.Flux.filter(Flux.java:4209)\n";
217+
218+
Traces.StackLineView view =
219+
new Traces.StackLineView(stackLine, 0, stackLine.length()).trim();
220+
221+
assertThat(view.withoutPublisherPackagePrefix()
222+
.toString()).isEqualTo("Flux.filter(Flux.java:4209)");
223+
}
149224
}

0 commit comments

Comments
 (0)