From 518d059a69ba1cf99797eff1808261f015696987 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Wed, 2 Oct 2024 21:56:34 +0200 Subject: [PATCH 1/8] Optimize `Traces#extractOperatorAssemblyInformationParts` This logic may be executed many times, e.g. if a hot code path uses `{Mono,Flux}#log` or Micrometer instrumentation. The added benchmark shows that for large stack traces the new implementation is several orders of magnitude more efficient in terms of compute and memory resource utilization. While there, improve two existing benchmarks by utilizing the black hole to which benchmark method return values are implicitly sent. --- .../core/publisher/MonoAllBenchmark.java | 6 +- .../core/publisher/MonoCallableBenchmark.java | 6 +- .../core/publisher/TracesBenchmark.java | 70 ++++++++++ .../java/reactor/core/publisher/Traces.java | 123 ++++++++++++------ 4 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java diff --git a/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java index 97f27110ea..9642bfb714 100644 --- a/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java +++ b/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,8 +49,8 @@ public static void main(String[] args) throws Exception { @SuppressWarnings("unused") @Benchmark - public void measureThroughput() { - Flux.range(0, rangeSize) + public Boolean measureThroughput() { + return Flux.range(0, rangeSize) .all(i -> i < Integer.MAX_VALUE) .block(); } diff --git a/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java index 886b9a2c43..79846a4adb 100644 --- a/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java +++ b/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,8 +49,8 @@ public static void main(String[] args) throws Exception { @SuppressWarnings("unused") @Benchmark - public void measureThroughput() { - Flux.range(0, rangeSize) + public Boolean measureThroughput() { + return Flux.range(0, rangeSize) .all(i -> i < Integer.MAX_VALUE) .block(); } diff --git a/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java new file mode 100644 index 0000000000..ec070ebe60 --- /dev/null +++ b/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class TracesBenchmark { + @Param({"0", "10", "100", "1000"}) + private int reactorLeadingLines; + + @Param({"0", "10", "100", "1000"}) + private int trailingLines; + + private String stackTrace; + + @Setup(Level.Iteration) + public void setup() { + stackTrace = createLargeStackTrace(reactorLeadingLines, trailingLines); + } + + @SuppressWarnings("unused") + @Benchmark + public String measureThroughput() { + return Traces.extractOperatorAssemblyInformation(stackTrace); + } + + private static String createLargeStackTrace(int reactorLeadingLines, int trailingLines) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < reactorLeadingLines; i++) { + sb.append("\tat reactor.core.publisher.Flux.someOperation(Flux.java:42)\n"); + } + sb.append("\tat some.user.package.SomeUserClass.someOperation(SomeUserClass.java:1234)\n"); + for (int i = 0; i < trailingLines; i++) { + sb.append("\tat any.package.AnyClass.anyOperation(AnyClass.java:1)\n"); + } + return sb.toString(); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index bac53fc126..61bcb6963f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package reactor.core.publisher; -import java.util.List; +import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - +import reactor.util.annotation.Nullable; /** * Utilities around manipulating stack traces and displaying assembly traces. @@ -29,6 +28,7 @@ * @author Sergei Egorov */ final class Traces { + private static final String PUBLISHER_PACKAGE_PREFIX = "reactor.core.publisher."; /** * If set to true, the creation of FluxOnAssembly will capture the raw stacktrace @@ -57,7 +57,6 @@ final class Traces { static boolean shouldSanitize(String stackTraceRow) { return stackTraceRow.startsWith("java.util.function") || stackTraceRow.startsWith("reactor.core.publisher.Mono.onAssembly") - || stackTraceRow.equals("reactor.core.publisher.Mono.onAssembly") || stackTraceRow.equals("reactor.core.publisher.Flux.onAssembly") || stackTraceRow.equals("reactor.core.publisher.ParallelFlux.onAssembly") || stackTraceRow.startsWith("reactor.core.publisher.SignalLogger") @@ -103,7 +102,7 @@ static String extractOperatorAssemblyInformation(String source) { } static boolean isUserCode(String line) { - return !line.startsWith("reactor.core.publisher") || line.contains("Test"); + return !line.startsWith(PUBLISHER_PACKAGE_PREFIX) || line.contains("Test"); } /** @@ -129,48 +128,92 @@ static boolean isUserCode(String line) { * from the assembly stack trace. */ static String[] extractOperatorAssemblyInformationParts(String source) { - String[] uncleanTraces = source.split("\n"); - final List traces = Stream.of(uncleanTraces) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); + Iterator traces = trimmedNonemptyLines(source); - if (traces.isEmpty()) { + if (!traces.hasNext()) { return new String[0]; } - int i = 0; - while (i < traces.size() && !isUserCode(traces.get(i))) { - i++; - } + String prevLine = null; + String currentLine = traces.next(); - String apiLine; - String userCodeLine; - if (i == 0) { - //no line was a reactor API line - apiLine = ""; - userCodeLine = traces.get(0); - } - else if (i == traces.size()) { - //we skipped ALL lines, meaning they're all reactor API lines. We'll fully display the last one - apiLine = ""; - userCodeLine = traces.get(i-1).replaceFirst("reactor.core.publisher.", ""); - } - else { - //currently on user code line, previous one is API - apiLine = traces.get(i - 1); - userCodeLine = traces.get(i); + if (isUserCode(currentLine)) { + // No line is a Reactor API line. + return new String[]{currentLine}; } - //now we want something in the form "Flux.map ⇢ user.code.Class.method(Class.java:123)" - if (apiLine.isEmpty()) return new String[] { userCodeLine }; + while (traces.hasNext()) { + prevLine = currentLine; + currentLine = traces.next(); + + if (isUserCode(currentLine)) { + // Currently on user code line, previous one is API. Attempt to create something in the form + // "Flux.map ⇢ user.code.Class.method(Class.java:123)". + int linePartIndex = prevLine.indexOf('('); + String apiLine = linePartIndex > 0 ? + prevLine.substring(0, linePartIndex) : + prevLine; - int linePartIndex = apiLine.indexOf('('); - if (linePartIndex > 0) { - apiLine = apiLine.substring(0, linePartIndex); + return new String[]{dropPublisherPackagePrefix(apiLine), "at " + currentLine}; + } } - apiLine = apiLine.replaceFirst("reactor.core.publisher.", ""); - return new String[] { apiLine, "at " + userCodeLine }; + // We skipped ALL lines, meaning they're all Reactor API lines. We'll fully display the last + // one. + return new String[]{dropPublisherPackagePrefix(currentLine)}; + } + + private static String dropPublisherPackagePrefix(String line) { + return line.startsWith(PUBLISHER_PACKAGE_PREFIX) + ? line.substring(PUBLISHER_PACKAGE_PREFIX.length()) + : line; + } + + /** + * Returns an iterator over all trimmed non-empty lines in the given source string. + * + * @implNote This implementation attempts to minimize allocations. + */ + private static Iterator trimmedNonemptyLines(String source) { + return new Iterator() { + private int index = 0; + @Nullable + private String next = getNextLine(); + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public String next() { + String current = next; + if (current == null) { + throw new NoSuchElementException(); + } + next = getNextLine(); + return current; + } + + @Nullable + private String getNextLine() { + if (index >= source.length()) { + return null; + } + + while (index < source.length()) { + int end = source.indexOf('\n', index); + if (end == -1) { + end = source.length(); + } + String line = source.substring(index, end).trim(); + index = end + 1; + if (!line.isEmpty()) { + return line; + } + } + return null; + } + }; } } From a67e27cba6b478abae650c46c0f1145fe53b0d62 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Thu, 3 Oct 2024 08:54:54 +0200 Subject: [PATCH 2/8] Defer `Scannable#name()` fallback logic (cherry picked from commit 009ec89f0a448ee67cee62a968491d0802c69183) --- reactor-core/src/main/java/reactor/core/Scannable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/Scannable.java b/reactor-core/src/main/java/reactor/core/Scannable.java index ca3b1143c3..b75cfaa183 100644 --- a/reactor-core/src/main/java/reactor/core/Scannable.java +++ b/reactor-core/src/main/java/reactor/core/Scannable.java @@ -449,7 +449,7 @@ default String name() { .map(s -> s.scan(Attr.NAME)) .filter(Objects::nonNull) .findFirst() - .orElse(stepName()); + .orElseGet(this::stepName); } /** From c3e519a75f69ffd932b7f7fdd851b2a5aa328858 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Tue, 26 Nov 2024 22:31:53 +0100 Subject: [PATCH 3/8] Update benchmark --- .../main/java/reactor/core/publisher/TracesBenchmark.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java index ec070ebe60..68d85b37c6 100644 --- a/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java +++ b/benchmarks/src/main/java/reactor/core/publisher/TracesBenchmark.java @@ -37,17 +37,17 @@ @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) public class TracesBenchmark { - @Param({"0", "10", "100", "1000"}) + @Param({"0", "1", "2"}) private int reactorLeadingLines; - @Param({"0", "10", "100", "1000"}) + @Param({"0", "1", "2"}) private int trailingLines; private String stackTrace; @Setup(Level.Iteration) public void setup() { - stackTrace = createLargeStackTrace(reactorLeadingLines, trailingLines); + stackTrace = createStackTrace(reactorLeadingLines, trailingLines); } @SuppressWarnings("unused") @@ -56,7 +56,7 @@ public String measureThroughput() { return Traces.extractOperatorAssemblyInformation(stackTrace); } - private static String createLargeStackTrace(int reactorLeadingLines, int trailingLines) { + private static String createStackTrace(int reactorLeadingLines, int trailingLines) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < reactorLeadingLines; i++) { sb.append("\tat reactor.core.publisher.Flux.someOperation(Flux.java:42)\n"); From 6f7d07944effb18a8fb8654dd0dff14fcb8155ef Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Thu, 28 Nov 2024 18:02:37 +0100 Subject: [PATCH 4/8] Address PR feedback --- .../src/main/java/reactor/core/publisher/Traces.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index 61bcb6963f..ddc2d27151 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -57,8 +57,8 @@ final class Traces { static boolean shouldSanitize(String stackTraceRow) { return stackTraceRow.startsWith("java.util.function") || stackTraceRow.startsWith("reactor.core.publisher.Mono.onAssembly") - || stackTraceRow.equals("reactor.core.publisher.Flux.onAssembly") - || stackTraceRow.equals("reactor.core.publisher.ParallelFlux.onAssembly") + || stackTraceRow.startsWith("reactor.core.publisher.Flux.onAssembly") + || stackTraceRow.startsWith("reactor.core.publisher.ParallelFlux.onAssembly") || stackTraceRow.startsWith("reactor.core.publisher.SignalLogger") || stackTraceRow.startsWith("reactor.core.publisher.FluxOnAssembly") || stackTraceRow.startsWith("reactor.core.publisher.MonoOnAssembly.") @@ -197,10 +197,6 @@ public String next() { @Nullable private String getNextLine() { - if (index >= source.length()) { - return null; - } - while (index < source.length()) { int end = source.indexOf('\n', index); if (end == -1) { From 7c8ce7b2dbabf26bd75b33a863ad6e841dfcf410 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Fri, 29 Nov 2024 20:27:33 +0100 Subject: [PATCH 5/8] Address PR feedback --- .../java/reactor/core/publisher/Traces.java | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index ddc2d27151..9fc3dce12b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -28,8 +28,6 @@ * @author Sergei Egorov */ final class Traces { - private static final String PUBLISHER_PACKAGE_PREFIX = "reactor.core.publisher."; - /** * If set to true, the creation of FluxOnAssembly will capture the raw stacktrace * instead of the sanitized version. @@ -47,6 +45,8 @@ final class Traces { */ static final Supplier> callSiteSupplierFactory = new CallSiteSupplierFactory(); + private static final String PUBLISHER_PACKAGE_PREFIX = "reactor.core.publisher."; + /** * Return true for strings (usually from a stack trace element) that should be * sanitized out by {@link Traces#callSiteSupplierFactory}. @@ -71,7 +71,7 @@ static boolean shouldSanitize(String stackTraceRow) { } /** - * Extract operator information out of an assembly stack trace in {@link String} form + * Extracts operator information out of an assembly stack trace in {@link String} form * (see {@link Traces#callSiteSupplierFactory}). *

* Most operators will result in a line of the form {@code "Flux.map ⇢ user.code.Class.method(Class.java:123)"}, @@ -82,11 +82,10 @@ static boolean shouldSanitize(String stackTraceRow) { * (eg. {@code "Flux.map"}) *

  • The next stacktrace element is considered user code and is appended to the * result with a {@code ⇢} separator. (eg. {@code " ⇢ user.code.Class.method(Class.java:123)"})
  • - *
  • If no user code is found in the sanitized stack, then the API reference is outputed in the later format only.
  • + *
  • If no user code is found in the sanitized stack, then the API reference is output in the later format only.
  • *
  • If the sanitized stack is empty, returns {@code "[no operator assembly information]"}
  • * * - * * @param source the sanitized assembly stacktrace in String format. * @return a {@link String} representing operator and operator assembly site extracted * from the assembly stack trace. @@ -106,26 +105,15 @@ static boolean isUserCode(String line) { } /** - * Extract operator information out of an assembly stack trace in {@link String} form - * (see {@link Traces#callSiteSupplierFactory}) which potentially - * has a header line that one can skip by setting {@code skipFirst} to {@code true}. + * Extracts operator information out of an assembly stack trace in {@link String} array form + * (see {@link Traces#callSiteSupplierFactory}). *

    - * Most operators will result in a line of the form {@code "Flux.map ⇢ user.code.Class.method(Class.java:123)"}, - * that is: - *

      - *
    1. The top of the stack is inspected for Reactor API references, and the deepest - * one is kept, since multiple API references generally denote an alias operator. - * (eg. {@code "Flux.map"})
    2. - *
    3. The next stacktrace element is considered user code and is appended to the - * result with a {@code ⇢} separator. (eg. {@code " ⇢ user.code.Class.method(Class.java:123)"})
    4. - *
    5. If no user code is found in the sanitized stack, then the API reference is outputed in the later format only.
    6. - *
    7. If the sanitized stack is empty, returns {@code "[no operator assembly information]"}
    8. - *
    - * + * The returned array will contain 0, 1 or 2 elements, extracted in a manner as described by + * {@link #extractOperatorAssemblyInformation(String)}. * * @param source the sanitized assembly stacktrace in String format. - * @return a {@link String} representing operator and operator assembly site extracted - * from the assembly stack trace. + * @return a 0-2 element string array containing the operator and operator assembly site extracted + * from the assembly stack trace */ static String[] extractOperatorAssemblyInformationParts(String source) { Iterator traces = trimmedNonemptyLines(source); From 02fa17e9068f7446583ce9462156031f7a7a8fe0 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Fri, 29 Nov 2024 22:35:50 +0100 Subject: [PATCH 6/8] Avoid `String#join` --- .../src/main/java/reactor/core/publisher/Traces.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index 9fc3dce12b..cc4a2332cf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -95,8 +95,12 @@ static String extractOperatorAssemblyInformation(String source) { switch (parts.length) { case 0: return "[no operator assembly information]"; + case 1: + return parts[0]; + case 2: + return parts[0] + CALL_SITE_GLUE + parts[1]; default: - return String.join(CALL_SITE_GLUE, parts); + throw new IllegalStateException("Unexpected number of assembly info parts: " + parts.length); } } From 36b3845a1ebd217b0de2dd7420ab7fa347ea2a4d Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Fri, 29 Nov 2024 23:44:05 +0100 Subject: [PATCH 7/8] Avoid copy on trim --- .../main/java/reactor/core/publisher/Traces.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index cc4a2332cf..918754981d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -194,7 +194,7 @@ private String getNextLine() { if (end == -1) { end = source.length(); } - String line = source.substring(index, end).trim(); + String line = trimmedSubstring(source, index, end); index = end + 1; if (!line.isEmpty()) { return line; @@ -202,6 +202,17 @@ private String getNextLine() { } return null; } + + // Equivalent to source.substring(start, end).trim(), but avoids allocation of a new array. + private String trimmedSubstring(String source, int start, int end) { + while (start < end && source.charAt(start) <= ' ') { + start++; + } + while (end > start && source.charAt(end - 1) <= ' ') { + end--; + } + return source.substring(start, end); + } }; } } From 9cef75f590f556ed34d463c43e515c66e1cc47ac Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Fri, 29 Nov 2024 22:24:14 +0100 Subject: [PATCH 8/8] Custom substring implementation --- .../java/reactor/core/publisher/Traces.java | 112 ++++++++++++------ 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index 918754981d..74e6b0d9d6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -120,45 +120,36 @@ static boolean isUserCode(String line) { * from the assembly stack trace */ static String[] extractOperatorAssemblyInformationParts(String source) { - Iterator traces = trimmedNonemptyLines(source); + Iterator traces = trimmedNonemptyLines(source); if (!traces.hasNext()) { return new String[0]; } - String prevLine = null; - String currentLine = traces.next(); + Substring prevLine = null; + Substring currentLine = traces.next(); - if (isUserCode(currentLine)) { + if (currentLine.isUserCode()) { // No line is a Reactor API line. - return new String[]{currentLine}; + return new String[]{currentLine.toString()}; } while (traces.hasNext()) { prevLine = currentLine; currentLine = traces.next(); - if (isUserCode(currentLine)) { + if (currentLine.isUserCode()) { // Currently on user code line, previous one is API. Attempt to create something in the form // "Flux.map ⇢ user.code.Class.method(Class.java:123)". - int linePartIndex = prevLine.indexOf('('); - String apiLine = linePartIndex > 0 ? - prevLine.substring(0, linePartIndex) : - prevLine; - - return new String[]{dropPublisherPackagePrefix(apiLine), "at " + currentLine}; + return new String[]{ + prevLine.withoutPublisherPackagePrefix().withoutLocationSuffix().toString(), + "at " + currentLine}; } } // We skipped ALL lines, meaning they're all Reactor API lines. We'll fully display the last // one. - return new String[]{dropPublisherPackagePrefix(currentLine)}; - } - - private static String dropPublisherPackagePrefix(String line) { - return line.startsWith(PUBLISHER_PACKAGE_PREFIX) - ? line.substring(PUBLISHER_PACKAGE_PREFIX.length()) - : line; + return new String[]{currentLine.withoutPublisherPackagePrefix().toString()}; } /** @@ -166,11 +157,11 @@ private static String dropPublisherPackagePrefix(String line) { * * @implNote This implementation attempts to minimize allocations. */ - private static Iterator trimmedNonemptyLines(String source) { - return new Iterator() { + private static Iterator trimmedNonemptyLines(String source) { + return new Iterator() { private int index = 0; @Nullable - private String next = getNextLine(); + private Substring next = getNextLine(); @Override public boolean hasNext() { @@ -178,8 +169,8 @@ public boolean hasNext() { } @Override - public String next() { - String current = next; + public Substring next() { + Substring current = next; if (current == null) { throw new NoSuchElementException(); } @@ -188,13 +179,13 @@ public String next() { } @Nullable - private String getNextLine() { + private Substring getNextLine() { while (index < source.length()) { int end = source.indexOf('\n', index); if (end == -1) { end = source.length(); } - String line = trimmedSubstring(source, index, end); + Substring line = new Substring(source, index, end).trim(); index = end + 1; if (!line.isEmpty()) { return line; @@ -202,17 +193,66 @@ private String getNextLine() { } return null; } + }; + } - // Equivalent to source.substring(start, end).trim(), but avoids allocation of a new array. - private String trimmedSubstring(String source, int start, int end) { - while (start < end && source.charAt(start) <= ' ') { - start++; - } - while (end > start && source.charAt(end - 1) <= ' ') { - end--; - } - return source.substring(start, end); + // XXX: Explain. + private static final class Substring { + private final String str; + private final int start; + private final int end; + + Substring(String str, int start, int end) { + this.str = str; + this.start = start; + this.end = end; + } + + Substring trim() { + int newStart = start; + while (newStart < end && str.charAt(newStart) <= ' ') { + newStart++; } - }; + int newEnd = end; + while (newEnd > newStart && str.charAt(newEnd - 1) <= ' ') { + newEnd--; + } + return newStart == start && newEnd == end ? this : new Substring(str, newStart, newEnd); + } + + boolean isEmpty() { + return start == end; + } + + boolean startsWith(String prefix) { + return str.startsWith(prefix, start); + } + + boolean contains(String substring) { + int index = str.indexOf(substring, start); + return index >= 0 && index < end; + } + + boolean isUserCode() { + return !startsWith(PUBLISHER_PACKAGE_PREFIX) || contains("Test"); + } + + Substring withoutLocationSuffix() { + int linePartIndex = str.indexOf('(', start); + return linePartIndex > 0 && linePartIndex < end + ? new Substring(str, start, linePartIndex) + : this; + } + + Substring withoutPublisherPackagePrefix() { + return startsWith(PUBLISHER_PACKAGE_PREFIX) + ? new Substring(str, start + PUBLISHER_PACKAGE_PREFIX.length(), end) + : this; + } + + @Override + public String toString() { + return str.substring(start, end); + } } }