diff --git a/src/java.base/share/classes/java/util/stream/AbstractPipeline.java b/src/java.base/share/classes/java/util/stream/AbstractPipeline.java index cdf8a605618..db75384e4d3 100644 --- a/src/java.base/share/classes/java/util/stream/AbstractPipeline.java +++ b/src/java.base/share/classes/java/util/stream/AbstractPipeline.java @@ -85,7 +85,7 @@ abstract class AbstractPipeline> * The "upstream" pipeline, or null if this is the source stage. */ @SuppressWarnings("rawtypes") - private final AbstractPipeline previousStage; + protected final AbstractPipeline previousStage; /** * The operation flags for the intermediate operation represented by this @@ -188,9 +188,13 @@ abstract class AbstractPipeline> * Constructor for appending an intermediate operation stage onto an * existing pipeline. * + * The previous stage must be unlinked and unconsumed. + * * @param previousStage the upstream pipeline stage * @param opFlags the operation flags for the new stage, described in * {@link StreamOpFlag} + * @throws IllegalStateException if previousStage is already linked or + * consumed */ AbstractPipeline(AbstractPipeline previousStage, int opFlags) { if (previousStage.linkedOrConsumed) @@ -205,6 +209,41 @@ abstract class AbstractPipeline> this.depth = previousStage.depth + 1; } + /** + * Constructor for replacing an intermediate operation stage onto an + * existing pipeline. + * + * @param previousPreviousStage the upstream pipeline stage of the upstream pipeline stage + * @param previousStage the upstream pipeline stage + * @param opFlags the operation flags for the new stage, described in + * {@link StreamOpFlag} + * @throws IllegalStateException if previousStage is already linked or + * consumed + */ + protected AbstractPipeline(AbstractPipeline previousPreviousStage, AbstractPipeline previousStage, int opFlags) { + if (previousStage.linkedOrConsumed || !previousPreviousStage.linkedOrConsumed || previousPreviousStage.nextStage != previousStage || previousStage.previousStage != previousPreviousStage) + throw new IllegalStateException(MSG_STREAM_LINKED); + + previousStage.linkedOrConsumed = true; + + previousPreviousStage.nextStage = this; + + this.previousStage = previousPreviousStage; + this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK; + this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousPreviousStage.combinedFlags); + this.sourceStage = previousPreviousStage.sourceStage; + this.depth = previousPreviousStage.depth + 1; + } + + /** + * Checks that the current stage has not been already linked or consumed, + * and then sets this stage as being linked or consumed. + */ + protected void linkOrConsume() { + if (linkedOrConsumed) + throw new IllegalStateException(MSG_STREAM_LINKED); + linkedOrConsumed = true; + } // Terminal evaluation methods @@ -402,7 +441,7 @@ abstract class AbstractPipeline> * operation. */ @SuppressWarnings("unchecked") - private Spliterator sourceSpliterator(int terminalFlags) { + protected Spliterator sourceSpliterator(int terminalFlags) { // Get the source spliterator of the pipeline Spliterator spliterator = null; if (sourceStage.sourceSpliterator != null) { @@ -740,6 +779,6 @@ abstract class AbstractPipeline> @SuppressWarnings("unchecked") Spliterator opEvaluateParallelLazy(PipelineHelper helper, Spliterator spliterator) { - return opEvaluateParallel(helper, spliterator, i -> (E_OUT[]) new Object[i]).spliterator(); + return opEvaluateParallel(helper, spliterator, Nodes.castingArray()).spliterator(); } } diff --git a/src/java.base/share/classes/java/util/stream/Gatherer.java b/src/java.base/share/classes/java/util/stream/Gatherer.java new file mode 100644 index 00000000000..ced746cd673 --- /dev/null +++ b/src/java.base/share/classes/java/util/stream/Gatherer.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util.stream; + +import jdk.internal.javac.PreviewFeature; +import jdk.internal.vm.annotation.ForceInline; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; + + +/** + * An intermediate operation that transforms a stream of input elements into a + * stream of output elements, optionally applying a final action when the end of + * the upstream is reached. The transformation may be stateless or stateful, + * and may buffer input before producing any output. + * + *

Gatherer operations can be performed either sequentially, + * or be parallelized -- if a combiner function is supplied. + * + *

There are many examples of gathering operations, including but not + * limited to: + * grouping elements into batches (windowing functions); + * de-duplicating consecutively similar elements; incremental accumulation + * functions (prefix scan); incremental reordering functions, etc. The class + * {@link java.util.stream.Gatherers} provides implementations of common + * gathering operations. + * + * @apiNote + *

A {@code Gatherer} is specified by four functions that work together to + * process input elements, optionally using intermediate state, and optionally + * perform a final action at the end of input. They are:

    + *
  • creating a new, potentially mutable, state ({@link #initializer()})
  • + *
  • integrating a new input element ({@link #integrator()})
  • + *
  • combining two states into one ({@link #combiner()})
  • + *
  • performing an optional final action ({@link #finisher()})
  • + *
+ * + *

Each invocation of {@link #initializer()}, {@link #integrator()}, + * {@link #combiner()}, and {@link #finisher()} must return a semantically + * identical result. + * + *

Implementations of Gatherer must not capture, retain, or expose to + * other threads, the references to the state instance, or the downstream + * {@link Downstream} for longer than the invocation duration of the method + * which they are passed to. + * + *

Performing a gathering operation with a {@code Gatherer} should produce a + * result equivalent to: + * + * {@snippet lang = java: + * Gatherer.Downstream downstream = ...; + * A state = gatherer.initializer().get(); + * for (T t : data) { + * gatherer.integrator().integrate(state, t, downstream); + * } + * gatherer.finisher().accept(state, downstream); + * } + * + *

However, the library is free to partition the input, perform the + * integrations on the partitions, and then use the combiner function to + * combine the partial results to achieve a gathering operation. (Depending + * on the specific gathering operation, this may perform better or worse, + * depending on the relative cost of the integrator and combiner functions.) + * + *

In addition to the predefined implementations in {@link Gatherers}, the + * static factory methods {@code of(...)} and {@code ofSequential(...)} + * can be used to construct gatherers. For example, you could create a gatherer + * that implements the equivalent of + * {@link java.util.stream.Stream#map(java.util.function.Function)} with: + * + * {@snippet lang = java: + * public static Gatherer map(Function mapper) { + * return Gatherer.of( + * (unused, element, downstream) -> // integrator + * downstream.push(mapper.apply(element)) + * ); + * } + * } + * + *

Gatherers are designed to be composed; two or more Gatherers can + * be composed into a single Gatherer using the {@link #andThen(Gatherer)} + * method. + * + * {@snippet lang = java: + * // using the implementation of `map` as seen above + * Gatherer increment = map(i -> i + 1); + * + * Gatherer toString = map(i -> i.toString()); + * + * Gatherer incrementThenToString = increment.andThen(toString); + * } + * + *

As an example, a Gatherer implementing a sequential Prefix Scan could + * be done the following way: + * + * {@snippet lang = java: + * public static Gatherer scan( + * Supplier initial, + * BiFunction scanner) { + * + * class State { + * R current = initial.get(); + * } + * + * return Gatherer.ofSequential( + * State::new, + * Gatherer.Integrator.ofGreedy((state, element, downstream) -> { + * state.current = scanner.apply(state.current, element); + * return downstream.push(state.current); + * }) + * ); + * } + * } + * + *

Example of usage: + * + * {@snippet lang = java: + * // will contain: ["1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "123456789"] + * List numberStrings = + * Stream.of(1,2,3,4,5,6,7,8,9) + * .gather( + * scan(() -> "", (string, number) -> string + number) + * ) + * .toList(); + * } + * + * @implSpec Libraries that implement transformations based on {@code Gatherer}, + * such as {@link Stream#gather(Gatherer)}, must adhere to the following + * constraints: + *

    + *
  • Gatherers whose initializer is {@link #defaultInitializer()} are + * considered to be stateless, and invoking their initializer is optional. + *
  • + *
  • Gatherers whose integrator is an instance of {@link Integrator.Greedy} + * can be assumed not to short-circuit, and the return value of invoking + * {@link Integrator#integrate(Object, Object, Downstream)} does not need to + * be inspected.
  • + *
  • The first argument passed to the integration function, both + * arguments passed to the combiner function, and the argument passed to the + * finisher function must be the result of a previous invocation of the + * initializer or combiner functions.
  • + *
  • The implementation should not do anything with the result of any of + * the initializer or combiner functions other than to + * pass them again to the integrator, combiner, or finisher functions.
  • + *
  • Once a state object is passed to the combiner or finisher function, + * it is never passed to the integrator function again.
  • + *
  • When the integrator function returns {@code false}, + * it shall be interpreted just as if there were no more elements to pass + * it.
  • + *
  • For parallel evaluation, the gathering implementation must manage + * that the input is properly partitioned, that partitions are processed + * in isolation, and combining happens only after integration is complete + * for both partitions.
  • + *
  • Gatherers whose combiner is {@link #defaultCombiner()} may only be + * evaluated sequentially. All other combiners allow the operation to be + * parallelized by initializing each partition in separation, invoking + * the integrator until it returns {@code false}, and then joining each + * partitions state using the combiner, and then invoking the finisher on + * the joined state. Outputs and state later in the input sequence will + * be discarded if processing an earlier partition short-circuits.
  • + *
  • Gatherers whose finisher is {@link #defaultFinisher()} are considered + * to not have an end-of-stream hook and invoking their finisher is + * optional.
  • + *
+ * + * @see Stream#gather(Gatherer) + * @see Gatherers + * + * @param the type of input elements to the gatherer operation + * @param the potentially mutable state type of the gatherer operation + * (often hidden as an implementation detail) + * @param the type of output elements from the gatherer operation + * @since 22 + */ +@PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) +public interface Gatherer { + /** + * A function that produces an instance of the intermediate state used for + * this gathering operation. + * + * @implSpec The implementation in this interface returns + * {@link #defaultInitializer()}. + * + * @return A function that produces an instance of the intermediate state + * used for this gathering operation + */ + default Supplier initializer() { + return defaultInitializer(); + }; + + /** + * A function which integrates provided elements, potentially using + * the provided intermediate state, optionally producing output to the + * provided {@link Downstream}. + * + * @return a function which integrates provided elements, potentially using + * the provided state, optionally producing output to the provided + * Downstream + */ + Integrator integrator(); + + /** + * A function which accepts two intermediate states and combines them into + * one. + * + * @implSpec The implementation in this interface returns + * {@link #defaultCombiner()}. + * + * @return a function which accepts two intermediate states and combines + * them into one + */ + default BinaryOperator combiner() { + return defaultCombiner(); + } + + /** + * A function which accepts the final intermediate state + * and a {@link Downstream} object, allowing to perform a final action at + * the end of input elements. + * + * @implSpec The implementation in this interface returns + * {@link #defaultFinisher()}. + * + * @return a function which transforms the intermediate result to the final + * result(s) which are then passed on to the provided Downstream + */ + default BiConsumer> finisher() { + return defaultFinisher(); + } + + /** + * Returns a composed Gatherer which connects the output of this Gatherer + * to the input of that Gatherer. + * + * @implSpec The implementation in this interface returns a new Gatherer + * which is semantically equivalent to the combination of + * {@code this} and {@code that} gatherer. + * + * @param that the other gatherer + * @param The type of output of that Gatherer + * @throws NullPointerException if the argument is {@code null} + * @return returns a composed Gatherer which connects the output of this + * Gatherer as input that Gatherer + */ + default Gatherer andThen(Gatherer that) { + Objects.requireNonNull(that); + return Gatherers.Composite.of(this, that); + } + + /** + * Returns an initializer which is the default initializer of a Gatherer. + * The returned initializer identifies that the owner Gatherer is stateless. + * + * @implSpec This method always returns the same instance. + * + * @see Gatherer#initializer() + * @return the instance of the default initializer + * @param the type of the state of the returned initializer + */ + static Supplier defaultInitializer() { + return Gatherers.Value.DEFAULT.initializer(); + } + + /** + * Returns a combiner which is the default combiner of a Gatherer. + * The returned combiner identifies that the owning Gatherer must only + * be evaluated sequentially. + * + * @implSpec This method always returns the same instance. + * + * @see Gatherer#finisher() + * @return the instance of the default combiner + * @param the type of the state of the returned combiner + */ + static BinaryOperator defaultCombiner() { + return Gatherers.Value.DEFAULT.combiner(); + } + + /** + * Returns a {@code finisher} which is the default finisher of + * a {@code Gatherer}. + * The returned finisher identifies that the owning Gatherer performs + * no additional actions at the end of input. + * + * @implSpec This method always returns the same instance. + * + * @see Gatherer#finisher() + * @return the instance of the default finisher + * @param the type of the state of the returned finisher + * @param the type of the Downstream of the returned finisher + */ + static BiConsumer> defaultFinisher() { + return Gatherers.Value.DEFAULT.finisher(); + } + + /** + * Returns a new, sequential, and stateless {@code Gatherer} described by + * the given {@code integrator}. + * + * @param integrator the integrator function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if the argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer ofSequential( + Integrator integrator) { + return of( + defaultInitializer(), + integrator, + defaultCombiner(), + defaultFinisher() + ); + } + + /** + * Returns a new, sequential, and stateless {@code Gatherer} described by + * the given {@code integrator} and {@code finisher}. + * + * @param integrator the integrator function for the new gatherer + * @param finisher the finisher function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer ofSequential( + Integrator integrator, + BiConsumer> finisher) { + return of( + defaultInitializer(), + integrator, + defaultCombiner(), + finisher + ); + } + + /** + * Returns a new, sequential, {@code Gatherer} described by the given + * {@code initializer} and {@code integrator}. + * + * @param initializer the initializer function for the new gatherer + * @param integrator the integrator function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of state for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer ofSequential( + Supplier initializer, + Integrator integrator) { + return of( + initializer, + integrator, + defaultCombiner(), + defaultFinisher() + ); + } + + /** + * Returns a new, sequential, {@code Gatherer} described by the given + * {@code initializer}, {@code integrator}, and {@code finisher}. + * + * @param initializer the initializer function for the new gatherer + * @param integrator the integrator function for the new gatherer + * @param finisher the finisher function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of state for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer ofSequential( + Supplier initializer, + Integrator integrator, + BiConsumer> finisher) { + return of( + initializer, + integrator, + defaultCombiner(), + finisher + ); + } + + /** + * Returns a new, parallelizable, and stateless {@code Gatherer} described + * by the given {@code integrator}. + * + * @param integrator the integrator function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer of(Integrator integrator) { + return of( + defaultInitializer(), + integrator, + Gatherers.Value.DEFAULT.statelessCombiner, + defaultFinisher() + ); + } + + /** + * Returns a new, parallelizable, and stateless {@code Gatherer} described + * by the given {@code integrator} and {@code finisher}. + * + * @param integrator the integrator function for the new gatherer + * @param finisher the finisher function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer of( + Integrator integrator, + BiConsumer> finisher) { + return of( + defaultInitializer(), + integrator, + Gatherers.Value.DEFAULT.statelessCombiner, + finisher + ); + } + + /** + * Returns a new, parallelizable, {@code Gatherer} described by the given + * {@code initializer}, {@code integrator}, {@code combiner} and + * {@code finisher}. + * + * @param initializer the initializer function for the new gatherer + * @param integrator the integrator function for the new gatherer + * @param combiner the combiner function for the new gatherer + * @param finisher the finisher function for the new gatherer + * @param the type of input elements for the new gatherer + * @param the type of state for the new gatherer + * @param the type of results for the new gatherer + * @throws NullPointerException if any argument is {@code null} + * @return the new {@code Gatherer} + */ + static Gatherer of( + Supplier initializer, + Integrator integrator, + BinaryOperator combiner, + BiConsumer> finisher) { + return new Gatherers.GathererImpl<>( + Objects.requireNonNull(initializer), + Objects.requireNonNull(integrator), + Objects.requireNonNull(combiner), + Objects.requireNonNull(finisher) + ); + } + + /** + * A Downstream object is the next stage in a pipeline of operations, + * to which elements can be sent. + * @param the type of elements this downstream accepts + * @since 22 + */ + @FunctionalInterface + @PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) + interface Downstream { + + /** + * Pushes, if possible, the provided element downstream -- to the next + * stage in the pipeline. + * + * @implSpec If this method returns {@code false} then no further + * elements will be accepted and subsequent invocations of this method + * will return {@code false}. + * + * @param element the element to push downstream + * @return {@code true} if more elements can be sent, + * and {@code false} if not. + */ + boolean push(T element); + + /** + * Checks whether the next stage is known to not want + * any more elements sent to it. + * + * @apiNote This is best-effort only, once this returns {@code true} it + * should never return {@code false} again for the same instance. + * + * @implSpec The implementation in this interface returns {@code false}. + * + * @return {@code true} if this Downstream is known not to want any + * more elements sent to it, {@code false} if otherwise + */ + default boolean isRejecting() { return false; } + } + + /** + * An Integrator receives elements and processes them, + * optionally using the supplied state, and optionally sends incremental + * results downstream. + * + * @param the type of state used by this integrator + * @param the type of elements this integrator consumes + * @param the type of results this integrator can produce + * @since 22 + */ + @FunctionalInterface + @PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) + interface Integrator { + /** + * Performs an action given: the current state, the next element, and + * a downstream object; potentially inspecting and/or updating + * the state, optionally sending any number of elements downstream + * -- and then returns whether more elements are to be consumed or not. + * + * @param state The state to integrate into + * @param element The element to integrate + * @param downstream The downstream object of this integration + * @return {@code true} if subsequent integration is desired, + * {@code false} if not + */ + boolean integrate(A state, T element, Downstream downstream); + + /** + * Factory method for turning Integrator-shaped lambdas into + * Integrators. + * + * @param integrator a lambda to create as Integrator + * @return the given lambda as an Integrator + * @param the type of state used by this integrator + * @param the type of elements this integrator receives + * @param the type of results this integrator can produce + */ + @ForceInline + static Integrator of(Integrator integrator) { + return integrator; + } + + /** + * Factory method for turning Integrator-shaped lambdas into + * {@link Greedy} Integrators. + * + * @param greedy a lambda to create as Integrator.Greedy + * @return the given lambda as a Greedy Integrator + * @param the type of state used by this integrator + * @param the type of elements this integrator receives + * @param the type of results this integrator can produce + */ + @ForceInline + static Greedy ofGreedy(Greedy greedy) { + return greedy; + } + + /** + * Greedy Integrators consume all their input, and may only relay that + * the downstream does not want more elements. + * + * @implSpec This interface is used to communicate that no + * short-circuiting will be initiated by this Integrator, and that + * information can then be used to optimize evaluation. + * + * @param the type of state used by this integrator + * @param the type of elements this greedy integrator receives + * @param the type of results this greedy integrator can produce + * @since 22 + */ + @FunctionalInterface + @PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) + interface Greedy extends Integrator { } + } +} diff --git a/src/java.base/share/classes/java/util/stream/GathererOp.java b/src/java.base/share/classes/java/util/stream/GathererOp.java new file mode 100644 index 00000000000..24b42fb28ad --- /dev/null +++ b/src/java.base/share/classes/java/util/stream/GathererOp.java @@ -0,0 +1,754 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util.stream; + +import jdk.internal.vm.annotation.ForceInline; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Optional; +import java.util.Spliterator; +import java.util.concurrent.CountedCompleter; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; +import java.util.stream.Gatherer.Integrator; + +/** + * Runtime machinery for evaluating Gatherers under different modes. + * The performance-critical code below contains some more complicated encodings: + * therefore, make sure to run benchmarks to verify changes to prevent regressions. + * + * @since 22 + */ +final class GathererOp extends ReferencePipeline { + @SuppressWarnings("unchecked") + static Stream of( + ReferencePipeline upstream, + Gatherer gatherer) { + // When attaching a gather-operation onto another gather-operation, + // we can fuse them into one + if (upstream.getClass() == GathererOp.class) { + return new GathererOp<>( + ((GathererOp) upstream).gatherer.andThen(gatherer), + (GathererOp) upstream); + } else { + return new GathererOp<>( + (ReferencePipeline) upstream, + gatherer); + } + } + + /* + * GathererOp.NodeBuilder is a lazy accumulator of elements with O(1) + * `append`, and O(8) `join` (concat). + * + * First `append` inflates a growable Builder, the O(8) for `join` is + * because we prefer to delegate to `append` for small concatenations to + * avoid excessive indirections (unbalanced Concat-trees) when joining many + * NodeBuilders together. + */ + static final class NodeBuilder implements Consumer { + private static final int LINEAR_APPEND_MAX = 8; // TODO revisit + static final class Builder extends SpinedBuffer implements Node { + Builder() { + } + } + + NodeBuilder() { + } + + private Builder rightMost; + private Node leftMost; + + private boolean isEmpty() { + return rightMost == null && leftMost == null; + } + + @Override + public void accept(X x) { + final var b = rightMost; + (b == null ? (rightMost = new NodeBuilder.Builder<>()) : b).accept(x); + } + + public NodeBuilder join(NodeBuilder that) { + if (isEmpty()) + return that; + + if (!that.isEmpty()) { + final var tb = that.build(); + if (rightMost != null && tb instanceof NodeBuilder.Builder + && tb.count() < LINEAR_APPEND_MAX) + tb.forEach(this); // Avoid conc for small nodes + else + leftMost = Nodes.conc(StreamShape.REFERENCE, this.build(), tb); + } + + return this; + } + + public Node build() { + if (isEmpty()) + return Nodes.emptyNode(StreamShape.REFERENCE); + + final var rm = rightMost; + + if (rm != null) { + rightMost = null; // Make sure builder isn't reused + final var lm = leftMost; + leftMost = (lm == null) ? rm : Nodes.conc(StreamShape.REFERENCE, lm, rm); + } + + return leftMost; + } + } + + static final class GatherSink implements Sink, Gatherer.Downstream { + private final Sink sink; + private final Gatherer gatherer; + private final Integrator integrator; // Optimization: reuse + private A state; + private boolean proceed = true; + + GatherSink(Gatherer gatherer, Sink sink) { + this.gatherer = gatherer; + this.sink = sink; + this.integrator = gatherer.integrator(); + } + + // java.util.stream.Sink contract below: + + @Override + public void begin(long size) { + final var initializer = gatherer.initializer(); + if (initializer != Gatherer.defaultInitializer()) // Optimization + state = initializer.get(); + sink.begin(size); + } + + @Override + public void accept(T t) { + /* Benchmarks have indicated that doing an unconditional write to + * `proceed` is more efficient than branching. + * We use `&=` here to prevent flips from `false` -> `true`. + * + * As of writing this, taking `greedy` or `stateless` into + * consideration at this point doesn't yield any performance gains. + */ + proceed &= integrator.integrate(state, t, this); + } + + @Override + public boolean cancellationRequested() { + return cancellationRequested(proceed); + } + + private boolean cancellationRequested(boolean knownProceed) { + // Highly performance sensitive + return !(knownProceed && (!sink.cancellationRequested() || (proceed = false))); + } + + @Override + public void end() { + final var finisher = gatherer.finisher(); + if (finisher != Gatherer.defaultFinisher()) // Optimization + finisher.accept(state, this); + sink.end(); + state = null; // GC assistance + } + + // Gatherer.Sink contract below: + + @Override + public boolean isRejecting() { + return !proceed; + } + + @Override + public boolean push(R r) { + var p = proceed; + if (p) + sink.accept(r); + return !cancellationRequested(p); + } + } + + private static int opFlagsFor(Integrator integrator) { + return integrator instanceof Integrator.Greedy + ? GREEDY_FLAGS : SHORT_CIRCUIT_FLAGS; + } + + private static final int DEFAULT_FLAGS = + StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT | + StreamOpFlag.NOT_SIZED; + + private static final int SHORT_CIRCUIT_FLAGS = + DEFAULT_FLAGS | StreamOpFlag.IS_SHORT_CIRCUIT; + + private static final int GREEDY_FLAGS = + DEFAULT_FLAGS; + + final Gatherer gatherer; + + /* + * This constructor is used for initial .gather() invocations + */ + private GathererOp(ReferencePipeline upstream, Gatherer gatherer) { + /* TODO this is a prime spot for pre-super calls to make sure that + * we only need to call `integrator()` once. + */ + super(upstream, opFlagsFor(gatherer.integrator())); + this.gatherer = gatherer; + } + + /* + * This constructor is used when fusing subsequent .gather() invocations + */ + @SuppressWarnings("unchecked") + private GathererOp(Gatherer gatherer, GathererOp upstream) { + super((AbstractPipeline) upstream.upstream(), + upstream, + opFlagsFor(gatherer.integrator())); + this.gatherer = gatherer; + } + + /* This allows internal access to the previous stage, + * to be able to fuse `gather` followed by `collect`. + */ + @SuppressWarnings("unchecked") + private AbstractPipeline upstream() { + return (AbstractPipeline) super.previousStage; + } + + @Override + boolean opIsStateful() { + // TODO + /* Currently GathererOp is always stateful, but what could be tried is: + * return gatherer.initializer() != Gatherer.defaultInitializer() + * || gatherer.combiner() == Gatherer.defaultCombiner() + * || gatherer.finisher() != Gatherer.defaultFinisher(); + */ + return true; + } + + @Override + Sink opWrapSink(int flags, Sink downstream) { + return new GatherSink<>(gatherer, downstream); + } + + /* + * This is used when evaluating .gather() operations interspersed with + * other Stream operations (in parallel) + */ + @Override + Node opEvaluateParallel(PipelineHelper unused1, + Spliterator spliterator, + IntFunction unused2) { + return this., Node>evaluate( + upstream().wrapSpliterator(spliterator), + true, + gatherer, + NodeBuilder::new, + NodeBuilder::accept, + NodeBuilder::join, + NodeBuilder::build + ); + } + + @Override + Spliterator opEvaluateParallelLazy(PipelineHelper helper, + Spliterator spliterator) { + /* + * There's a very small subset of possible Gatherers which would be + * expressible as Spliterators directly, + * - the Gatherer's initializer is Gatherer.defaultInitializer(), + * - the Gatherer's combiner is NOT Gatherer.defaultCombiner() + * - the Gatherer's finisher is Gatherer.defaultFinisher() + */ + return opEvaluateParallel(null, spliterator, null).spliterator(); + } + + /* gather-operations immediately followed by (terminal) collect-operations + * are fused together to avoid having to first run the gathering to + * completion and only after that be able to run the collection on top of + * the output. This is highly beneficial in the parallel case as stateful + * operations cannot be pipelined in the ReferencePipeline implementation. + * Overriding collect-operations overcomes this limitation. + */ + @Override + public CR collect(Collector c) { + linkOrConsume(); // Important for structural integrity + final var parallel = isParallel(); + final var u = upstream(); + return evaluate( + u.wrapSpliterator(u.sourceSpliterator(0)), + parallel, + gatherer, + c.supplier(), + c.accumulator(), + parallel ? c.combiner() : null, + c.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH) + ? null + : c.finisher() + ); + } + + @Override + public RR collect(Supplier supplier, + BiConsumer accumulator, + BiConsumer combiner) { + linkOrConsume(); // Important for structural integrity + final var parallel = isParallel(); + final var u = upstream(); + return evaluate( + u.wrapSpliterator(u.sourceSpliterator(0)), + parallel, + gatherer, + supplier, + accumulator, + parallel ? (l, r) -> { + combiner.accept(l, r); + return l; + } : null, + null + ); + } + + /* + * evaluate(...) is the primary execution mechanism besides opWrapSink() + * and implements both sequential, hybrid parallel-sequential, and + * parallel evaluation + */ + private CR evaluate(final Spliterator spliterator, + final boolean parallel, + final Gatherer gatherer, + final Supplier collectorSupplier, + final BiConsumer collectorAccumulator, + final BinaryOperator collectorCombiner, + final Function collectorFinisher) { + + // There are two main sections here: sequential and parallel + + final var initializer = gatherer.initializer(); + final var integrator = gatherer.integrator(); + + // Optimization + final boolean greedy = integrator instanceof Integrator.Greedy; + + // Sequential evaluation section starts here. + + // Sequential is the fusion of a Gatherer and a Collector which can + // be evaluated sequentially. + final class Sequential implements Consumer, Gatherer.Downstream { + A state; + CA collectorState; + boolean proceed; + + Sequential() { + if (initializer != Gatherer.defaultInitializer()) + state = initializer.get(); + collectorState = collectorSupplier.get(); + proceed = true; + } + + @ForceInline + Sequential evaluateUsing(Spliterator spliterator) { + if (greedy) + spliterator.forEachRemaining(this); + else + do { + } while (proceed && spliterator.tryAdvance(this)); + + return this; + } + + /* + * No need to override isKnownDone() as the default is `false` + * and collectors can never short-circuit. + */ + @Override + public boolean push(R r) { + collectorAccumulator.accept(collectorState, r); + return true; + } + + @Override + public void accept(T t) { + /* + * Benchmarking has shown that, in this case, conditional + * writing of `proceed` is desirable and if that was not the + * case, then the following line would've been clearer: + * + * proceed &= integrator.integrate(state, t, this); + */ + + var ignore = integrator.integrate(state, t, this) + || (!greedy && (proceed = false)); + } + + @SuppressWarnings("unchecked") + public CR get() { + final var finisher = gatherer.finisher(); + if (finisher != Gatherer.defaultFinisher()) + finisher.accept(state, this); + // IF collectorFinisher == null -> IDENTITY_FINISH + return (collectorFinisher == null) + ? (CR) collectorState + : collectorFinisher.apply(collectorState); + } + } + + /* + * It could be considered to also go to sequential mode if the + * operation is non-greedy AND the combiner is Gatherer.defaultCombiner() + * as those operations will not benefit from upstream parallel + * preprocessing which is the main advantage of the Hybrid evaluation + * strategy. + */ + if (!parallel) + return new Sequential().evaluateUsing(spliterator).get(); + + // Parallel section starts here: + + final var combiner = gatherer.combiner(); + + /* + * The following implementation of hybrid parallel-sequential + * Gatherer processing borrows heavily from ForeachOrderedTask, + * and adds handling of short-circuiting. + */ + @SuppressWarnings("serial") + final class Hybrid extends CountedCompleter { + private final long targetSize; + private final Hybrid leftPredecessor; + private final AtomicBoolean cancelled; + private final Sequential localResult; + + private Spliterator spliterator; + private Hybrid next; + + private static final VarHandle NEXT; + + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + NEXT = l.findVarHandle(Hybrid.class, "next", Hybrid.class); + } catch (Exception e) { + throw new InternalError(e); + } + } + + protected Hybrid(Spliterator spliterator) { + super(null); + this.spliterator = spliterator; + this.targetSize = + AbstractTask.suggestTargetSize(spliterator.estimateSize()); + this.localResult = new Sequential(); + this.cancelled = greedy ? null : new AtomicBoolean(false); + this.leftPredecessor = null; + } + + Hybrid(Hybrid parent, Spliterator spliterator, Hybrid leftPredecessor) { + super(parent); + this.spliterator = spliterator; + this.targetSize = parent.targetSize; + this.localResult = parent.localResult; + this.cancelled = parent.cancelled; + this.leftPredecessor = leftPredecessor; + } + + @Override + public Sequential getRawResult() { + return localResult; + } + + @Override + public void setRawResult(Sequential result) { + if (result != null) throw new IllegalStateException(); + } + + @Override + public void compute() { + var task = this; + Spliterator rightSplit = task.spliterator, leftSplit; + long sizeThreshold = task.targetSize; + boolean forkRight = false; + while ((greedy || !cancelled.get()) + && rightSplit.estimateSize() > sizeThreshold + && (leftSplit = rightSplit.trySplit()) != null) { + + var leftChild = new Hybrid(task, leftSplit, task.leftPredecessor); + var rightChild = new Hybrid(task, rightSplit, leftChild); + + /* leftChild and rightChild were just created and not + * fork():ed yet so no need for a volatile write + */ + leftChild.next = rightChild; + + // Fork the parent task + // Completion of the left and right children "happens-before" + // completion of the parent + task.addToPendingCount(1); + // Completion of the left child "happens-before" completion of + // the right child + rightChild.addToPendingCount(1); + + // If task is not on the left spine + if (task.leftPredecessor != null) { + /* + * Completion of left-predecessor, or left subtree, + * "happens-before" completion of left-most leaf node of + * right subtree. + * The left child's pending count needs to be updated before + * it is associated in the completion map, otherwise the + * left child can complete prematurely and violate the + * "happens-before" constraint. + */ + leftChild.addToPendingCount(1); + // Update association of left-predecessor to left-most + // leaf node of right subtree + if (NEXT.compareAndSet(task.leftPredecessor, task, leftChild)) { + // If replaced, adjust the pending count of the parent + // to complete when its children complete + task.addToPendingCount(-1); + } else { + // Left-predecessor has already completed, parent's + // pending count is adjusted by left-predecessor; + // left child is ready to complete + leftChild.addToPendingCount(-1); + } + } + + if (forkRight) { + rightSplit = leftSplit; + task = leftChild; + rightChild.fork(); + } else { + task = rightChild; + leftChild.fork(); + } + forkRight = !forkRight; + } + + /* + * Task's pending count is either 0 or 1. If 1 then the completion + * map will contain a value that is task, and two calls to + * tryComplete are required for completion, one below and one + * triggered by the completion of task's left-predecessor in + * onCompletion. Therefore there is no data race within the if + * block. + * + * IMPORTANT: Currently we only perform the processing of this + * upstream data if we know the operation is greedy -- as we cannot + * safely speculate on the cost/benefit ratio of parallelizing + * the pre-processing of upstream data under short-circuiting. + */ + if (greedy && task.getPendingCount() > 0) { + // Upstream elements are buffered + NodeBuilder nb = new NodeBuilder<>(); + rightSplit.forEachRemaining(nb); // Run the upstream + task.spliterator = nb.build().spliterator(); + } + task.tryComplete(); + } + + @Override + public void onCompletion(CountedCompleter caller) { + var s = spliterator; + spliterator = null; // GC assistance + + /* Performance sensitive since each leaf-task could have a + * spliterator of size 1 which means that all else is overhead + * which needs minimization. + */ + if (s != null + && (greedy || !cancelled.get()) + && !localResult.evaluateUsing(s).proceed + && !greedy) + cancelled.set(true); + + // The completion of this task *and* the dumping of elements + // "happens-before" completion of the associated left-most leaf task + // of right subtree (if any, which can be this task's right sibling) + @SuppressWarnings("unchecked") + var leftDescendant = (Hybrid) NEXT.getAndSet(this, null); + if (leftDescendant != null) { + leftDescendant.tryComplete(); + } + } + } + + /* + * The following implementation of parallel Gatherer processing + * borrows heavily from AbstractShortCircuitTask + */ + @SuppressWarnings("serial") + final class Parallel extends CountedCompleter { + private Spliterator spliterator; + private Parallel leftChild; // Only non-null if rightChild is + private Parallel rightChild; // Only non-null if leftChild is + private Sequential localResult; + private volatile boolean canceled; + private long targetSize; // lazily initialized + + private Parallel(Parallel parent, Spliterator spliterator) { + super(parent); + this.targetSize = parent.targetSize; + this.spliterator = spliterator; + } + + Parallel(Spliterator spliterator) { + super(null); + this.targetSize = 0L; + this.spliterator = spliterator; + } + + private long getTargetSize(long sizeEstimate) { + long s; + return ((s = targetSize) != 0 + ? s + : (targetSize = AbstractTask.suggestTargetSize(sizeEstimate))); + } + + @Override + public Sequential getRawResult() { + return localResult; + } + + @Override + public void setRawResult(Sequential result) { + if (result != null) throw new IllegalStateException(); + } + + private void doProcess() { + if (!(localResult = new Sequential()).evaluateUsing(spliterator).proceed + && !greedy) + cancelLaterTasks(); + } + + @Override + public void compute() { + Spliterator rs = spliterator, ls; + long sizeEstimate = rs.estimateSize(); + final long sizeThreshold = getTargetSize(sizeEstimate); + Parallel task = this; + boolean forkRight = false; + boolean proceed; + while ((proceed = (greedy || !task.isRequestedToCancel())) + && sizeEstimate > sizeThreshold + && (ls = rs.trySplit()) != null) { + final var leftChild = task.leftChild = new Parallel(task, ls); + final var rightChild = task.rightChild = new Parallel(task, rs); + task.setPendingCount(1); + if (forkRight) { + rs = ls; + task = leftChild; + rightChild.fork(); + } else { + task = rightChild; + leftChild.fork(); + } + forkRight = !forkRight; + sizeEstimate = rs.estimateSize(); + } + if (proceed) + task.doProcess(); + task.tryComplete(); + } + + Sequential merge(Sequential l, Sequential r) { + /* + * Only join the right if the left side didn't short-circuit, + * or when greedy + */ + if (greedy || (l != null && r != null && l.proceed)) { + l.state = combiner.apply(l.state, r.state); + l.collectorState = + collectorCombiner.apply(l.collectorState, r.collectorState); + l.proceed = r.proceed; + return l; + } + + return (l != null) ? l : r; + } + + @Override + public void onCompletion(CountedCompleter caller) { + spliterator = null; // GC assistance + if (leftChild != null) { + /* Results can only be null in the case where there's + * short-circuiting or when Gatherers are stateful but + * uses `null` as their state value. + */ + localResult = merge(leftChild.localResult, rightChild.localResult); + leftChild = rightChild = null; // GC assistance + } + } + + @SuppressWarnings("unchecked") + private Parallel getParent() { + return (Parallel) getCompleter(); + } + + private boolean isRequestedToCancel() { + boolean cancel = canceled; + if (!cancel) { + for (Parallel parent = getParent(); + !cancel && parent != null; + parent = parent.getParent()) + cancel = parent.canceled; + } + return cancel; + } + + private void cancelLaterTasks() { + // Go up the tree, cancel right siblings of this node and all parents + for (Parallel parent = getParent(), node = this; + parent != null; + node = parent, parent = parent.getParent()) { + // If node is a left child of parent, then has a right sibling + if (parent.leftChild == node) + parent.rightChild.canceled = true; + } + } + } + + if (combiner != Gatherer.defaultCombiner()) + return new Parallel(spliterator).invoke().get(); + else + return new Hybrid(spliterator).invoke().get(); + } +} \ No newline at end of file diff --git a/src/java.base/share/classes/java/util/stream/Gatherers.java b/src/java.base/share/classes/java/util/stream/Gatherers.java new file mode 100644 index 00000000000..c201ee54609 --- /dev/null +++ b/src/java.base/share/classes/java/util/stream/Gatherers.java @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.util.stream; + +import jdk.internal.access.SharedSecrets; +import jdk.internal.javac.PreviewFeature; +import jdk.internal.vm.annotation.ForceInline; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.Semaphore; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Gatherer.Integrator; +import java.util.stream.Gatherer.Downstream; + +/** + * Implementations of {@link Gatherer} that provide useful intermediate + * operations, such as windowing functions, folding functions, + * transforming elements concurrently, etc. + * + * @since 22 +*/ +@PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) +public final class Gatherers { + private Gatherers() { } // This class is not intended to be instantiated + + // Public built-in Gatherers and factory methods for them + + /** + * Returns a Gatherer that gathers elements into windows + * -- encounter-ordered groups of elements -- of a fixed size. + * If the stream is empty then no window will be produced. + * The last window may contain fewer elements than the supplied window size. + * + *

Example: + * {@snippet lang = java: + * // will contain: [[1, 2, 3], [4, 5, 6], [7, 8]] + * List> windows = + * Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowFixed(3)).toList(); + * } + * + * @implSpec Each window produced is an unmodifiable List; calls to any + * mutator method will always cause {@code UnsupportedOperationException} + * to be thrown. There are no guarantees on the implementation type or + * serializability of the produced Lists. + * + * @apiNote For efficiency reasons, windows may be allocated contiguously + * and eagerly. This means that choosing large window sizes for + * small streams may use excessive memory for the duration of + * evaluation of this operation. + * + * @param windowSize the size of the windows + * @param the type of elements the returned gatherer consumes + * and the contents of the windows it produces + * @return a new gatherer which groups elements into fixed-size windows + * @throws IllegalArgumentException when {@code windowSize} is less than 1 + */ + public static Gatherer> windowFixed(int windowSize) { + if (windowSize < 1) + throw new IllegalArgumentException("'windowSize' must be greater than zero"); + + class FixedWindow { + Object[] window; + int at; + + FixedWindow() { + at = 0; + window = new Object[windowSize]; + } + + boolean integrate(TR element, Downstream> downstream) { + window[at++] = element; + if (at < windowSize) { + return true; + } else { + final var oldWindow = window; + window = new Object[windowSize]; + at = 0; + return downstream.push( + SharedSecrets.getJavaUtilCollectionAccess() + .listFromTrustedArrayNullsAllowed(oldWindow) + ); + } + } + + void finish(Downstream> downstream) { + if (at > 0 && !downstream.isRejecting()) { + var lastWindow = new Object[at]; + System.arraycopy(window, 0, lastWindow, 0, at); + window = null; + at = 0; + downstream.push( + SharedSecrets.getJavaUtilCollectionAccess() + .listFromTrustedArrayNullsAllowed(lastWindow) + ); + } + } + } + return Gatherer.>ofSequential( + // Initializer + FixedWindow::new, + + // Integrator + Integrator.>ofGreedy(FixedWindow::integrate), + + // Finisher + FixedWindow::finish + ); + } + + /** + * Returns a Gatherer that gathers elements into windows -- + * encounter-ordered groups of elements -- of a given size, where each + * subsequent window includes all elements of the previous window except + * for the least recent, and adds the next element in the stream. + * If the stream is empty then no window will be produced. If the size of + * the stream is smaller than the window size then only one window will + * be produced, containing all elements in the stream. + * + *

Example: + * {@snippet lang = java: + * // will contain: [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8]] + * List> windows2 = + * Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowSliding(2)).toList(); + * + * // will contain: [[1, 2, 3, 4, 5, 6], [2, 3, 4, 5, 6, 7], [3, 4, 5, 6, 7, 8]] + * List> windows6 = + * Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowSliding(6)).toList(); + * } + * + * @implSpec Each window produced is an unmodifiable List; calls to any + * mutator method will always cause {@code UnsupportedOperationException} + * to be thrown. There are no guarantees on the implementation type or + * serializability of the produced Lists. + * + * @apiNote For efficiency reasons, windows may be allocated contiguously + * and eagerly. This means that choosing large window sizes for + * small streams may use excessive memory for the duration of + * evaluation of this operation. + * + * @param windowSize the size of the windows + * @param the type of elements the returned gatherer consumes + * and the contents of the windows it produces + * @return a new gatherer which groups elements into sliding windows + * @throws IllegalArgumentException when windowSize is less than 1 + */ + public static Gatherer> windowSliding(int windowSize) { + if (windowSize < 1) + throw new IllegalArgumentException("'windowSize' must be greater than zero"); + + class SlidingWindow { + Object[] window; + int at; + boolean firstWindow; + + SlidingWindow() { + firstWindow = true; + at = 0; + window = new Object[windowSize]; + } + + boolean integrate(TR element, Downstream> downstream) { + window[at++] = element; + if (at < windowSize) { + return true; + } else { + final var oldWindow = window; + final var newWindow = new Object[windowSize]; + System.arraycopy(oldWindow,1, newWindow, 0, windowSize - 1); + window = newWindow; + at -= 1; + firstWindow = false; + return downstream.push( + SharedSecrets.getJavaUtilCollectionAccess() + .listFromTrustedArrayNullsAllowed(oldWindow) + ); + } + } + + void finish(Downstream> downstream) { + if (firstWindow && at > 0 && !downstream.isRejecting()) { + var lastWindow = new Object[at]; + System.arraycopy(window, 0, lastWindow, 0, at); + window = null; + at = 0; + downstream.push( + SharedSecrets.getJavaUtilCollectionAccess() + .listFromTrustedArrayNullsAllowed(lastWindow) + ); + } + } + } + return Gatherer.>ofSequential( + // Initializer + SlidingWindow::new, + + // Integrator + Integrator.>ofGreedy(SlidingWindow::integrate), + + // Finisher + SlidingWindow::finish + ); + } + + /** + * Returns a Gatherer that performs an ordered, reduction-like, + * transformation for scenarios where no combiner-function can be + * implemented, or for reductions which are intrinsically + * order-dependent. + * + * @implSpec If no exceptions are thrown during processing, then this + * operation only ever produces a single element. + * + *

Example: + * {@snippet lang = java: + * // will contain: Optional["123456789"] + * Optional numberString = + * Stream.of(1,2,3,4,5,6,7,8,9) + * .gather( + * Gatherers.fold(() -> "", (string, number) -> string + number) + * ) + * .findFirst(); + * } + * + * @see java.util.stream.Stream#reduce(Object, BinaryOperator) + * + * @param initial the identity value for the fold operation + * @param folder the folding function + * @param the type of elements the returned gatherer consumes + * @param the type of elements the returned gatherer produces + * @return a new Gatherer + * @throws NullPointerException if any of the parameters are {@code null} + */ + public static Gatherer fold( + Supplier initial, + BiFunction folder) { + Objects.requireNonNull(initial, "'initial' must not be null"); + Objects.requireNonNull(folder, "'folder' must not be null"); + + class State { + R value = initial.get(); + State() {} + } + + return Gatherer.ofSequential( + State::new, + Integrator.ofGreedy((state, element, downstream) -> { + state.value = folder.apply(state.value, element); + return true; + }), + (state, downstream) -> downstream.push(state.value) + ); + } + + /** + * Returns a Gatherer that performs a Prefix Scan -- an incremental + * accumulation -- using the provided functions. Starting with an + * initial value obtained from the {@code Supplier}, each subsequent + * value is obtained by applying the {@code BiFunction} to the current + * value and the next input element, after which the resulting value is + * produced downstream. + * + *

Example: + * {@snippet lang = java: + * // will contain: ["1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "123456789"] + * List numberStrings = + * Stream.of(1,2,3,4,5,6,7,8,9) + * .gather( + * Gatherers.scan(() -> "", (string, number) -> string + number) + * ) + * .toList(); + * } + * + * @param initial the supplier of the initial value for the scanner + * @param scanner the function to apply for each element + * @param the type of element which this gatherer consumes + * @param the type of element which this gatherer produces + * @return a new Gatherer which performs a prefix scan + * @throws NullPointerException if any of the parameters are {@code null} + */ + public static Gatherer scan( + Supplier initial, + BiFunction scanner) { + Objects.requireNonNull(initial, "'initial' must not be null"); + Objects.requireNonNull(scanner, "'scanner' must not be null"); + + class State { + R current = initial.get(); + boolean integrate(T element, Downstream downstream) { + return downstream.push(current = scanner.apply(current, element)); + } + } + + return Gatherer.ofSequential(State::new, + Integrator.ofGreedy(State::integrate)); + } + + /** + * An operation which executes a function concurrently + * with a configured level of max concurrency, using + * virtual threads. + * This operation preserves the ordering of the stream. + * + * @apiNote In progress tasks will be attempted to be cancelled, + * on a best-effort basis, in situations where the downstream no longer + * wants to receive any more elements. + * + * @implSpec If a result of the function is to be pushed downstream but + * instead the function completed exceptionally then the corresponding + * exception will instead be rethrown by this method as an instance of + * {@link RuntimeException}, after which any remaining tasks are canceled. + * + * @param maxConcurrency the maximum concurrency desired + * @param mapper a function to be executed concurrently + * @param the type of input + * @param the type of output + * @return a new Gatherer + * @throws IllegalArgumentException if {@code maxConcurrency} is less than 1 + * @throws NullPointerException if {@code mapper} is {@code null} + */ + public static Gatherer mapConcurrent( + final int maxConcurrency, + final Function mapper) { + if (maxConcurrency < 1) + throw new IllegalArgumentException( + "'maxConcurrency' must be greater than 0"); + + Objects.requireNonNull(mapper, "'mapper' must not be null"); + + class State { + // ArrayDeque default initial size is 16 + final ArrayDeque> window = + new ArrayDeque<>(Math.min(maxConcurrency, 16)); + final Semaphore windowLock = new Semaphore(maxConcurrency); + + final boolean integrate(T element, + Downstream downstream) { + if (!downstream.isRejecting()) + createTaskFor(element); + return flush(0, downstream); + } + + final void createTaskFor(T element) { + windowLock.acquireUninterruptibly(); + + var task = new FutureTask(() -> { + try { + return mapper.apply(element); + } finally { + windowLock.release(); + } + }); + + var wasAddedToWindow = window.add(task); + assert wasAddedToWindow; + + Thread.startVirtualThread(task); + } + + final boolean flush(long atLeastN, + Downstream downstream) { + boolean proceed = !downstream.isRejecting(); + boolean interrupted = false; + try { + Future current; + while (proceed + && (current = window.peek()) != null + && (current.isDone() || atLeastN > 0)) { + proceed &= downstream.push(current.get()); + atLeastN -= 1; + + var correctRemoval = window.pop() == current; + assert correctRemoval; + } + } catch(InterruptedException ie) { + proceed = false; + interrupted = true; + } catch (ExecutionException e) { + proceed = false; // Ensure cleanup + final var cause = e.getCause(); + throw (cause instanceof RuntimeException re) + ? re + : new RuntimeException(cause == null ? e : cause); + } finally { + // Clean up + if (!proceed) { + Future next; + while ((next = window.pollFirst()) != null) { + next.cancel(true); + } + } + } + + if (interrupted) + Thread.currentThread().interrupt(); + + return proceed; + } + } + + return Gatherer.ofSequential( + State::new, + Integrator.ofGreedy(State::integrate), + (state, downstream) -> state.flush(Long.MAX_VALUE, downstream) + ); + } + + // Implementation details + + /* + * This enum is used to provide the default functions for the + * factory methods + * and for the default methods for when implementing the Gatherer interface. + * + * This serves the following purposes: + * 1. removes the need for using `null` for signalling absence of specified + * value and thereby hiding user bugs + * 2. allows to check against these default values to avoid calling methods + * needlessly + * 3. allows for more efficient composition and evaluation + */ + @SuppressWarnings("rawtypes") + enum Value implements Supplier, BinaryOperator, BiConsumer { + DEFAULT; + + final BinaryOperator statelessCombiner = new BinaryOperator<>() { + @Override public Void apply(Void left, Void right) { return null; } + }; + + // BiConsumer + @Override public void accept(Object state, Object downstream) {} + + // BinaryOperator + @Override public Object apply(Object left, Object right) { + throw new UnsupportedOperationException("This combiner cannot be used!"); + } + + // Supplier + @Override public Object get() { return null; } + + @ForceInline + @SuppressWarnings("unchecked") + Supplier initializer() { return (Supplier)this; } + + @ForceInline + @SuppressWarnings("unchecked") + BinaryOperator combiner() { return (BinaryOperator) this; } + + @ForceInline + @SuppressWarnings("unchecked") + BiConsumer> finisher() { + return (BiConsumer>) this; + } + } + + record GathererImpl( + @Override Supplier initializer, + @Override Integrator integrator, + @Override BinaryOperator combiner, + @Override BiConsumer> finisher) implements Gatherer { + + static GathererImpl of( + Supplier initializer, + Integrator integrator, + BinaryOperator combiner, + BiConsumer> finisher) { + return new GathererImpl<>( + Objects.requireNonNull(initializer,"initializer"), + Objects.requireNonNull(integrator, "integrator"), + Objects.requireNonNull(combiner, "combiner"), + Objects.requireNonNull(finisher, "finisher") + ); + } + } + + static final class Composite implements Gatherer { + private final Gatherer left; + private final Gatherer right; + // FIXME change `impl` to a computed constant when available + private GathererImpl impl; + + static Composite of( + Gatherer left, + Gatherer right) { + return new Composite<>(left, right); + } + + private Composite(Gatherer left, + Gatherer right) { + this.left = left; + this.right = right; + } + + @SuppressWarnings("unchecked") + private GathererImpl impl() { + // ATTENTION: this method currently relies on a "benign" data-race + // as it should deterministically produce the same result even if + // initialized concurrently on different threads. + var i = impl; + return i != null + ? i + : (impl = (GathererImpl)impl(left, right)); + } + + @Override public Supplier initializer() { + return impl().initializer(); + } + + @Override public Integrator integrator() { + return impl().integrator(); + } + + @Override public BinaryOperator combiner() { + return impl().combiner(); + } + + @Override public BiConsumer> finisher() { + return impl().finisher(); + } + + @Override + public Gatherer andThen( + Gatherer that) { + if (that.getClass() == Composite.class) { + @SuppressWarnings("unchecked") + final var c = + (Composite) that; + return left.andThen(right.andThen(c.left).andThen(c.right)); + } else { + return left.andThen(right.andThen(that)); + } + } + + static final GathererImpl impl( + Gatherer left, Gatherer right) { + final var leftInitializer = left.initializer(); + final var leftIntegrator = left.integrator(); + final var leftCombiner = left.combiner(); + final var leftFinisher = left.finisher(); + + final var rightInitializer = right.initializer(); + final var rightIntegrator = right.integrator(); + final var rightCombiner = right.combiner(); + final var rightFinisher = right.finisher(); + + final var leftStateless = leftInitializer == Gatherer.defaultInitializer(); + final var rightStateless = rightInitializer == Gatherer.defaultInitializer(); + + final var leftGreedy = leftIntegrator instanceof Integrator.Greedy; + final var rightGreedy = rightIntegrator instanceof Integrator.Greedy; + + /* + * For pairs of stateless and greedy Gatherers, we can optimize + * evaluation as we do not need to track any state nor any + * short-circuit signals. This can provide significant + * performance improvements. + */ + if (leftStateless && rightStateless && leftGreedy && rightGreedy) { + return new GathererImpl<>( + Gatherer.defaultInitializer(), + Gatherer.Integrator.ofGreedy((unused, element, downstream) -> + leftIntegrator.integrate( + null, + element, + r -> rightIntegrator.integrate(null, r, downstream)) + ), + (leftCombiner == Gatherer.defaultCombiner() + || rightCombiner == Gatherer.defaultCombiner()) + ? Gatherer.defaultCombiner() + : Value.DEFAULT.statelessCombiner + , + (leftFinisher == Gatherer.defaultFinisher() + && rightFinisher == Gatherer.defaultFinisher()) + ? Gatherer.defaultFinisher() + : (unused, downstream) -> { + if (leftFinisher != Gatherer.defaultFinisher()) + leftFinisher.accept( + null, + r -> rightIntegrator.integrate(null, r, downstream)); + if (rightFinisher != Gatherer.defaultFinisher()) + rightFinisher.accept(null, downstream); + } + ); + } else { + class State { + final A leftState; + final AA rightState; + boolean leftProceed; + boolean rightProceed; + + private State(A leftState, AA rightState, + boolean leftProceed, boolean rightProceed) { + this.leftState = leftState; + this.rightState = rightState; + this.leftProceed = leftProceed; + this.rightProceed = rightProceed; + } + + State() { + this(leftStateless ? null : leftInitializer.get(), + rightStateless ? null : rightInitializer.get(), + true, true); + } + + State joinLeft(State right) { + return new State( + leftStateless ? null : leftCombiner.apply(this.leftState, right.leftState), + rightStateless ? null : rightCombiner.apply(this.rightState, right.rightState), + this.leftProceed && this.rightProceed, + right.leftProceed && right.rightProceed); + } + + boolean integrate(T t, Downstream c) { + /* + * rightProceed must be checked after integration of + * left since that can cause right to short-circuit + * We always want to conditionally write leftProceed + * here, which means that we only do so if we are + * known to be not-greedy. + */ + return (leftIntegrator.integrate(leftState, t, r -> rightIntegrate(r, c)) + || leftGreedy + || (leftProceed = false)) + && (rightGreedy || rightProceed); + } + + void finish(Downstream c) { + if (leftFinisher != Gatherer.defaultFinisher()) + leftFinisher.accept(leftState, r -> rightIntegrate(r, c)); + if (rightFinisher != Gatherer.defaultFinisher()) + rightFinisher.accept(rightState, c); + } + + /* + * Currently we use the following to ferry elements from + * the left Gatherer to the right Gatherer, but we create + * the Gatherer.Downstream as a lambda which means that + * the default implementation of `isKnownDone()` is used. + * + * If it is determined that we want to be able to support + * the full interface of Gatherer.Downstream then we have + * the following options: + * 1. Have State implement Downstream + * and store the passed in Downstream + * downstream as an instance field in integrate() + * and read it in push(R r). + * 2. Allocate a new Gatherer.Downstream for + * each invocation of integrate() which might prove + * costly. + */ + public boolean rightIntegrate(R r, Downstream downstream) { + // The following logic is highly performance sensitive + return (rightGreedy || rightProceed) + && (rightIntegrator.integrate(rightState, r, downstream) + || rightGreedy + || (rightProceed = false)); + } + } + + return new GathererImpl( + State::new, + (leftGreedy && rightGreedy) + ? Integrator.ofGreedy(State::integrate) + : Integrator.of(State::integrate), + (leftCombiner == Gatherer.defaultCombiner() + || rightCombiner == Gatherer.defaultCombiner()) + ? Gatherer.defaultCombiner() + : State::joinLeft, + (leftFinisher == Gatherer.defaultFinisher() + && rightFinisher == Gatherer.defaultFinisher()) + ? Gatherer.defaultFinisher() + : State::finish + ); + } + } + } +} diff --git a/src/java.base/share/classes/java/util/stream/ReferencePipeline.java b/src/java.base/share/classes/java/util/stream/ReferencePipeline.java index e0b9c79025b..5058440335f 100644 --- a/src/java.base/share/classes/java/util/stream/ReferencePipeline.java +++ b/src/java.base/share/classes/java/util/stream/ReferencePipeline.java @@ -90,12 +90,27 @@ abstract class ReferencePipeline * Constructor for appending an intermediate operation onto an existing * pipeline. * - * @param upstream the upstream element source. + * @param upstream the upstream element source + * @param opFlags The operation flags for this operation, described in + * {@link StreamOpFlag} */ ReferencePipeline(AbstractPipeline upstream, int opFlags) { super(upstream, opFlags); } + /** + * Constructor for appending an intermediate operation onto an existing + * pipeline. + * + * @param upupstream the upstream of the upstream element source + * @param upstream the upstream element source + * @param opFlags The operation flags for this operation, described in + * {@link StreamOpFlag} + */ + protected ReferencePipeline(AbstractPipeline upupstream, AbstractPipeline upstream, int opFlags) { + super(upupstream, upstream, opFlags); + } + // Shape-specific methods @Override @@ -667,9 +682,14 @@ abstract class ReferencePipeline return evaluate(ReduceOps.makeRef(identity, accumulator, combiner)); } + @Override + public final Stream gather(Gatherer gatherer) { + return GathererOp.of(this, gatherer); + } + @Override @SuppressWarnings("unchecked") - public final R collect(Collector collector) { + public R collect(Collector collector) { A container; if (isParallel() && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT)) @@ -687,7 +707,7 @@ abstract class ReferencePipeline } @Override - public final R collect(Supplier supplier, + public R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) { return evaluate(ReduceOps.makeRef(supplier, accumulator, combiner)); diff --git a/src/java.base/share/classes/java/util/stream/Stream.java b/src/java.base/share/classes/java/util/stream/Stream.java index 284ec632d74..30b87bd8b54 100644 --- a/src/java.base/share/classes/java/util/stream/Stream.java +++ b/src/java.base/share/classes/java/util/stream/Stream.java @@ -24,6 +24,8 @@ */ package java.util.stream; +import jdk.internal.javac.PreviewFeature; + import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -1051,6 +1053,58 @@ public interface Stream extends BaseStream> { BiFunction accumulator, BinaryOperator combiner); + /** + * Returns a stream consisting of the results of applying the given + * {@link Gatherer} to the elements of this stream. + * + *

This is a stateful + * intermediate operation that is an + * extension point. + * + *

Gatherers are highly flexible and can describe a vast array of + * possibly stateful operations, with support for short-circuiting, and + * parallelization. + * + *

When executed in parallel, multiple intermediate results may be + * instantiated, populated, and merged so as to maintain isolation of + * mutable data structures. Therefore, even when executed in parallel + * with non-thread-safe data structures (such as {@code ArrayList}), no + * additional synchronization is needed for a parallel reduction. + * + *

Implementations are allowed, but not required, to detect consecutive + * invocations and compose them into a single, fused, operation. This would + * make the first expression below behave like the second: + * + *

{@code
+     *     var stream1 = Stream.of(...).gather(gatherer1).gather(gatherer2);
+     *     var stream2 = Stream.of(...).gather(gatherer1.andThen(gatherer2));
+     * }
+ * + * @implSpec + * The default implementation obtains the {@link #spliterator() spliterator} + * of this stream, wraps that spliterator so as to support the semantics + * of this operation on traversal, and returns a new stream associated with + * the wrapped spliterator. The returned stream preserves the execution + * characteristics of this stream (namely parallel or sequential execution + * as per {@link #isParallel()}) but the wrapped spliterator may choose to + * not support splitting. When the returned stream is closed, the close + * handlers for both the returned and this stream are invoked. + * Implementations of this interface should provide their own + * implementation of this method. + * + * @see Gatherers + * @param The element type of the new stream + * @param gatherer a gatherer + * @return the new stream + * @since 22 + */ + @PreviewFeature(feature = PreviewFeature.Feature.STREAM_GATHERERS) + default Stream gather(Gatherer gatherer) { + return StreamSupport.stream(spliterator(), isParallel()) + .gather(gatherer) + .onClose(this::close); + } + /** * Performs a mutable * reduction operation on the elements of this stream. A mutable diff --git a/src/java.base/share/classes/java/util/stream/package-info.java b/src/java.base/share/classes/java/util/stream/package-info.java index 85700bfbab1..951dc41ea44 100644 --- a/src/java.base/share/classes/java/util/stream/package-info.java +++ b/src/java.base/share/classes/java/util/stream/package-info.java @@ -620,6 +620,19 @@ * but in some cases equivalence may be relaxed to account for differences in * order. * + *

Extensibility

+ * + *

Implementing {@link java.util.stream.Collector}; + * using the factory method {@code java.util.stream.Collector.of(...)}; or + * using the predefined collectors in {@link java.util.stream.Collectors} allows + * for user-defined, reusable, terminal operations. + * + *

Implementing {@link java.util.stream.Gatherer}; using the factory + * methods {@code java.util.stream.Gatherer.of(...)} and + * {@code java.util.stream.Gatherer.ofSequential(...)}; + * or using the predefined gatherers in {@link java.util.stream.Gatherers} + * allows for user-defined, reusable, intermediate operations. + * *

Reduction, concurrency, and ordering

* * With some complex reduction operations, for example a {@code collect()} that diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java index f7ce449b981..77dec6dce49 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -77,6 +77,8 @@ public @interface PreviewFeature { SCOPED_VALUES, @JEP(number=453, title="Structured Concurrency", status="Preview") STRUCTURED_CONCURRENCY, + @JEP(number=461, title="Stream Gatherers", status="Preview") + STREAM_GATHERERS, /** * A key for testing. */ diff --git a/test/jdk/java/util/stream/GathererAPITest.java b/test/jdk/java/util/stream/GathererAPITest.java new file mode 100644 index 00000000000..4bcda85ccbc --- /dev/null +++ b/test/jdk/java/util/stream/GathererAPITest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; +import java.util.stream.*; +import java.util.stream.Gatherer; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; + +/** + * @test + * @summary Testing public API of Gatherer + * @enablePreview + * @run junit GathererAPITest + */ + +public class GathererAPITest { + final static Supplier initializer = () -> (Void)null; + final static Gatherer.Integrator integrator = (v,e,d) -> d.push(e); + final static BinaryOperator combiner = (l,r) -> l; + final static BiConsumer> finisher = (v,d) -> {}; + + final static Supplier nullInitializer = null; + final static Gatherer.Integrator nullIntegrator = null; + final static BinaryOperator nullCombiner = null; + final static BiConsumer> nullFinisher = null; + + private final static Gatherer passthrough() { + return Gatherer.of( + () -> (Void)null, + Gatherer.Integrator.ofGreedy((v,e,d) -> d.push(e)), + (l,r) -> l, + (v,d) -> {} + ); + } + + private final static Gatherer verifyGathererContract(Gatherer gatherer) { + // basics + assertNotNull(gatherer); + + // components + assertNotNull(gatherer.initializer()); + assertNotNull(gatherer.integrator()); + assertNotNull(gatherer.combiner()); + assertNotNull(gatherer.finisher()); + assertNotNull(gatherer.andThen(passthrough())); + + return gatherer; + } + + private final static Gatherer verifyGathererStructure( + Gatherer gatherer, + Supplier expectedSupplier, + Gatherer.Integrator expectedIntegrator, + BinaryOperator expectedCombiner, + BiConsumer> expectedFinisher + ) { + // basics + assertNotNull(gatherer); + + // components + assertSame(expectedSupplier, gatherer.initializer()); + assertSame(expectedIntegrator, gatherer.integrator()); + assertSame(expectedCombiner, gatherer.combiner()); + assertSame(expectedFinisher, gatherer.finisher()); + + return gatherer; + } + + @Test + public void testGathererDefaults() { + final Gatherer.Integrator expectedIntegrator = + (a,b,c) -> false; + + class Test implements Gatherer { + @Override + public Integrator integrator() { + return expectedIntegrator; + } + } + + var t = new Test(); + assertSame(Gatherer.defaultInitializer(), t.initializer()); + assertSame(expectedIntegrator, t.integrator()); + assertSame(Gatherer.defaultCombiner(), t.combiner()); + assertSame(Gatherer.>defaultFinisher(), t.finisher()); + } + + @Test + public void testDownstreamDefaults() { + class Test implements Gatherer.Downstream { + @Override public boolean push(Void v) { return false; } + } + + var t = new Test(); + assertEquals(false, t.isRejecting()); + } + + @Test + public void testGathererFactoriesNPE() { + assertThrows(NullPointerException.class, + () -> Gatherer.of(nullInitializer, integrator, combiner, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.of(initializer, nullIntegrator, combiner, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.of(initializer, integrator, nullCombiner, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.of(initializer, integrator, combiner, nullFinisher)); + + assertThrows(NullPointerException.class, + () -> Gatherer.of(nullIntegrator)); + + assertThrows(NullPointerException.class, + () -> Gatherer.of(nullIntegrator, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.of(integrator, nullFinisher)); + + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(nullInitializer, integrator)); + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(initializer, nullIntegrator)); + + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(nullIntegrator)); + + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(nullIntegrator, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(integrator, nullFinisher)); + + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(nullInitializer, integrator, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(initializer, nullIntegrator, finisher)); + assertThrows(NullPointerException.class, + () -> Gatherer.ofSequential(initializer, integrator, nullFinisher)); + } + + @Test + public void testGathererFactoriesAPI() { + final var defaultInitializer = Gatherer.defaultInitializer(); + final var defaultCombiner = Gatherer.defaultCombiner(); + final var defaultFinisher = Gatherer.defaultFinisher(); + + var g1 = verifyGathererContract(passthrough()); // Quis custodiet ipsos custodes? + verifyGathererContract(g1.andThen(g1)); + + var g2 = verifyGathererContract(Gatherer.of(integrator)); + verifyGathererContract(g2.andThen(g2)); + assertSame(defaultInitializer, g2.initializer()); + assertSame(integrator, g2.integrator()); + assertNotSame(defaultCombiner, g2.combiner()); + assertSame(defaultFinisher, g2.finisher()); + + var g3 = verifyGathererContract(Gatherer.of(integrator, finisher)); + verifyGathererContract(g3.andThen(g3)); + assertSame(integrator, g3.integrator()); + assertNotSame(defaultCombiner, g3.combiner()); + assertSame(finisher, g3.finisher()); + + var g4 = verifyGathererContract(Gatherer.ofSequential(integrator)); + verifyGathererContract(g4.andThen(g4)); + verifyGathererStructure(g4, defaultInitializer, integrator, defaultCombiner, defaultFinisher); + + var g5 = verifyGathererContract(Gatherer.ofSequential(initializer, integrator)); + verifyGathererContract(g5.andThen(g5)); + verifyGathererStructure(g5, initializer, integrator, defaultCombiner, defaultFinisher); + + var g6 = verifyGathererContract(Gatherer.ofSequential(integrator, finisher)); + verifyGathererContract(g6.andThen(g6)); + verifyGathererStructure(g6, defaultInitializer, integrator, defaultCombiner, finisher); + + var g7 = verifyGathererContract(Gatherer.ofSequential(initializer, integrator, finisher)); + verifyGathererContract(g7.andThen(g7)); + verifyGathererStructure(g7, initializer, integrator, defaultCombiner, finisher); + + var g8 = verifyGathererContract(Gatherer.of(initializer, integrator, combiner, finisher)); + verifyGathererContract(g8.andThen(g8)); + verifyGathererStructure(g8, initializer, integrator, combiner, finisher); + } + + @Test + public void testGathererVariance() { + + // Make sure that Gatherers can pass-through type + Gatherer nums = Gatherer.of((unused, element, downstream) -> downstream.push(element)); + + // Make sure that Gatherers can upcast the output type from the input type + Gatherer upcast = Gatherer.of((unused, element, downstream) -> downstream.push(element)); + + // Make sure that Gatherers can consume a supertype of the Stream output + assertEquals(List.of(1,2,3,4,5), Stream.of(1,2,3,4,5).gather(nums).toList()); + + Gatherer ints = Gatherer.of((unused, element, downstream) -> downstream.push(element)); + + // Make sure that Gatherers can be composed where the output is a subtype of the input type of the next + Gatherer composition = ints.andThen(nums); + + // Make sure that composition works transitively, typing-wise + Gatherer upcastComposition = ints.andThen(nums.andThen(upcast)); + + assertEquals(List.of(1,2,3,4,5), Stream.of(1,2,3,4,5).gather(composition).toList()); + assertEquals(List.of(1,2,3,4,5), Stream.of(1,2,3,4,5).gather(upcastComposition).toList()); + } +} diff --git a/test/jdk/java/util/stream/GathererTest.java b/test/jdk/java/util/stream/GathererTest.java new file mode 100644 index 00000000000..5d5f0b517aa --- /dev/null +++ b/test/jdk/java/util/stream/GathererTest.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.*; +import java.util.stream.Gatherer; +import static java.util.stream.DefaultMethodStreams.delegateTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; + +/** + * @test + * @summary Testing the Gatherer contract + * @enablePreview + * @library /lib/testlibrary/bootlib + * @build java.base/java.util.stream.DefaultMethodStreams + * @run junit GathererTest + */ + +public class GathererTest { + + record Config(int streamSize, boolean parallel, boolean defaultImpl) { + + Stream countTo(int n) { + return Stream.iterate(1, i -> i + 1).limit(n); + } + + Stream stream() { + return wrapStream(countTo(streamSize)); + } + + Stream wrapStream(Stream stream) { + stream = parallel ? stream.parallel() : stream.sequential(); + stream = defaultImpl ? delegateTo(stream) : stream; + return stream; + } + + List list() { + return stream().toList(); + } + } + + final static Stream configurations() { + return Stream.of(0,1,10,33,99,9999) + .flatMap(size -> + Stream.of(false, true) + .flatMap(parallel -> + Stream.of(false, true).map( defaultImpl -> + new Config(size, parallel, + defaultImpl)) ) + ); + } + + final class TestException extends RuntimeException { + TestException(String message) { + super(message); + } + } + + final static class InvocationTracker { + int initialize; + int integrate; + int combine; + int finish; + + void copyFrom(InvocationTracker other) { + initialize = other.initialize; + integrate = other.integrate; + combine = other.combine; + finish = other.finish; + } + + void combine(InvocationTracker other) { + if (other != this) { + initialize += other.initialize; + integrate += other.integrate; + combine += other.combine + 1; // track this merge + finish += other.finish; + } + } + } + + final Gatherer addOne = Gatherer.of( + Gatherer.Integrator.ofGreedy((vöid, element, downstream) -> downstream.push(element + 1)) + ); + + final Gatherer timesTwo = Gatherer.of( + Gatherer.Integrator.ofGreedy((vöid, element, downstream) -> downstream.push(element * 2)) + ); + + @ParameterizedTest + @MethodSource("configurations") + public void testInvocationSemanticsGreedy(Config config) { + var result = new InvocationTracker(); + var g = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + Gatherer.Integrator.ofGreedy((t, e, d) -> { + t.integrate++; + return d.push(e); + }), + (t1, t2) -> { + t1.combine(t2); + return t1; + }, + (t, d) -> { + t.finish++; + result.copyFrom(t); + }); + var res = config.stream().gather(g).toList(); + assertEquals(config.countTo(config.streamSize).toList(), res); + if (config.parallel) { + assertTrue(result.initialize > 0); + assertEquals(config.streamSize, result.integrate); + assertTrue(config.streamSize < 2 || result.combine > 0); + assertEquals(1, result.finish); + } else { + assertEquals(1, result.initialize); + assertEquals(config.streamSize, result.integrate); + assertEquals(0, result.combine); + assertEquals(1, result.finish); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testInvocationSemanticsShortCircuit(Config config) { + final int CONSUME_UNTIL = Math.min(config.streamSize, 5); + var result = new InvocationTracker(); + var g = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + (t, e, d) -> { + ++t.integrate; + return e <= CONSUME_UNTIL && d.push(e) && e != CONSUME_UNTIL; + }, + (t1, t2) -> { + t1.combine(t2); + return t1; + }, + (t, d) -> { + t.finish++; + result.copyFrom(t); + }); + var res = config.stream().gather(g).toList(); + assertEquals(config.countTo(CONSUME_UNTIL).toList(), res); + if (config.parallel) { + assertTrue(result.initialize > 0); + assertEquals(CONSUME_UNTIL, result.integrate); + assertTrue(result.combine >= 0); // We can't guarantee split sizes + assertEquals(1, result.finish); + } else { + assertEquals(1, result.initialize); + assertEquals(CONSUME_UNTIL, result.integrate); + assertEquals(0, result.combine); + assertEquals(1, result.finish); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testEmissionDuringFinisher(Config config) { + var g = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + (t, e, d) -> { + t.integrate++; + return true; + }, + (t1, t2) -> { + t1.combine(t2); + return t1; + }, + (t, d) -> { + t.finish++; + d.push(t); + }); + var resultList = config.stream().gather(g).collect(Collectors.toList()); + assertEquals(resultList.size(), 1); + + var t = resultList.get(0); + + if (config.parallel) { + assertTrue(t.initialize > 0); + assertEquals(config.streamSize, t.integrate); + assertTrue(config.streamSize < 2 || t.combine > 0); + assertEquals(1, t.finish); + } else { + assertEquals(1, t.initialize); + assertEquals(config.streamSize, t.integrate); + assertEquals(0, t.combine); + assertEquals(1, t.finish); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testInvocationSemanticsShortCircuitDuringCollect(Config config) { + final int CONSUME_UNTIL = Math.min(config.streamSize, 5); + var result = new InvocationTracker(); + var g = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + (t, e, d) -> { + t.integrate++; + return e <= CONSUME_UNTIL && d.push(e) && e != CONSUME_UNTIL; + }, + (t1, t2) -> { + t1.combine(t2); + return t1; + }, + (t, d) -> { + t.finish++; + result.copyFrom(t); + }); + var res = config.stream().gather(g).collect(Collectors.toList()); + assertEquals(config.countTo(CONSUME_UNTIL).toList(), res); + if (config.parallel) { + assertTrue(result.initialize > 0); + assertEquals(CONSUME_UNTIL, result.integrate); + assertTrue(result.combine >= 0); // We can't guarantee split sizes + assertEquals(result.finish, 1); + } else { + assertEquals(result.initialize, 1); + assertEquals(CONSUME_UNTIL, result.integrate); + assertEquals(result.combine, 0); + assertEquals(result.finish, 1); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testCompositionOfStatelessGatherers(Config config) { + var range = config.stream().toList(); + var gRes = range.stream().gather(addOne.andThen(timesTwo)).toList(); + var rRes = range.stream().map(j -> j + 1).map(j -> j * 2).toList(); + assertEquals(config.streamSize, gRes.size()); + assertEquals(config.streamSize, rRes.size()); + assertEquals(gRes, rRes); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testCompositionOfStatefulGatherers(Config config) { + var t1 = new InvocationTracker(); + var g1 = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + (t, e, d) -> { + t.integrate++; + return d.push(e); + }, + (l, r) -> { + l.combine(r); + return l; + }, + (t, d) -> { + t.finish++; + t1.copyFrom(t); + }); + + var t2 = new InvocationTracker(); + var g2 = Gatherer.of( + () -> { + var t = new InvocationTracker(); + t.initialize++; + return t; + }, + (t, e, d) -> { + t.integrate++; + return d.push(e); + }, + (l, r) -> { + l.combine(r); + return l; + }, + (t, d) -> { + t.finish++; + t2.copyFrom(t); + }); + + var res = config.stream().gather(g1.andThen(g2)).toList(); + assertEquals(config.stream().toList(), res); + + if (config.parallel) { + assertTrue(t1.initialize > 0); + assertEquals(config.streamSize, t1.integrate); + assertTrue(config.streamSize < 2 || t1.combine > 0); + assertEquals(1, t1.finish); + + assertTrue(t2.initialize > 0); + assertEquals(config.streamSize, t2.integrate); + assertTrue(config.streamSize < 2 || t2.combine > 0); + assertEquals(1, t2.finish); + } else { + assertEquals(1, t1.initialize); + assertEquals(config.streamSize, t1.integrate); + assertEquals(0, t1.combine); + assertEquals(1, t1.finish); + + assertEquals(1, t2.initialize); + assertEquals(config.streamSize, t2.integrate); + assertEquals(0, t2.combine); + assertEquals(1, t2.finish); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testMassivelyComposedGatherers(Config config) { + final int ITERATIONS = 512; // Total number of compositions is 1 + (iterations*2) + Gatherer g = addOne; + for(int i = 0;i < ITERATIONS;++i) { + g = g.andThen(timesTwo).andThen(addOne); + } + + g = g.andThen(timesTwo); + + var ref = config.stream().map(n -> n + 1); + for(int c = 0; c < ITERATIONS; ++c) { + ref = ref.map(n -> n * 2).map(n -> n + 1); + } + ref = ref.map(n -> n * 2); + + var gatherered = config.stream().gather(g).toList(); + var reference = ref.toList(); + assertEquals(gatherered, reference); + } + + @Test + public void testUnboundedEmissions() { + Gatherer g = Gatherer.of( + () -> (Void)null, + (v,e,d) -> { do {} while(d.push(e)); return false; }, + (l,r) -> l, + (v,d) -> {} + ); + assertEquals(Stream.of(1).gather(g).limit(1).toList(), List.of(1)); + assertEquals(Stream.of(1).gather(g.andThen(g)).limit(1).toList(), List.of(1)); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testCompositionSymmetry(Config config) { + var consecutiveResult = config.stream().gather(addOne).gather(timesTwo).toList(); + var interspersedResult = config.stream().gather(addOne).map(id -> id).gather(timesTwo).toList(); + var composedResult = config.stream().gather(addOne.andThen(timesTwo)).toList(); + + var reference = config.stream().map(j -> j + 1).map(j -> j * 2).toList(); + + assertEquals(config.streamSize, consecutiveResult.size()); + assertEquals(config.streamSize, interspersedResult.size()); + assertEquals(config.streamSize, composedResult.size()); + assertEquals(config.streamSize, reference.size()); + + assertEquals(consecutiveResult, reference); + assertEquals(interspersedResult, reference); + assertEquals(composedResult, reference); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testExceptionInInitializer(Config config) { + final var expectedMessage = "testExceptionInInitializer()"; + assertThrowsTestException(() -> + config.stream().gather( + Gatherer.of( + () -> { throw new TestException(expectedMessage); }, + (i, e, d) -> true, + (l,r) -> l, + (i,d) -> {} + ) + ).toList(), expectedMessage); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testExceptionInIntegrator(Config config) { + if (config.streamSize < 1) return; // No exceptions expected + + final var expectedMessage = "testExceptionInIntegrator()"; + assertThrowsTestException(() -> + config.stream().gather( + Gatherer.of( + () -> 1, + (i, e, d) -> { throw new TestException(expectedMessage); }, + (l,r) -> l, + (i,d) -> {} + ) + ).toList() + , expectedMessage); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testExceptionInCombiner(Config config) { + if (config.streamSize < 2 || !config.parallel) return; // No exceptions expected + + final var expectedMessage = "testExceptionInCombiner()"; + assertThrowsTestException(() -> + config.stream().gather( + Gatherer.of( + () -> 1, + (i, e, d) -> true, + (l,r) -> { throw new TestException(expectedMessage); }, + (i,d) -> {} + ) + ).toList() + , expectedMessage); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testExceptionInFinisher(Config config) { + final var expectedMessage = "testExceptionInFinisher()"; + assertThrowsTestException(() -> + config.stream().gather( + Gatherer.of( + () -> 1, + (i, e, d) -> true, + (l,r) -> l, + (v, d) -> { throw new TestException(expectedMessage); } + ) + ).toList() + , expectedMessage); + } + + private final static void assertThrowsTestException(Supplier supplier, String expectedMessage) { + try { + var discard = supplier.get(); + } catch (TestException e) { + assertSame(TestException.class, e.getClass()); + assertEquals(expectedMessage, e.getMessage()); + return; + } + fail("Expected TestException but wasn't thrown!"); + } +} diff --git a/test/jdk/java/util/stream/GatherersTest.java b/test/jdk/java/util/stream/GatherersTest.java new file mode 100644 index 00000000000..e2cbee38fc2 --- /dev/null +++ b/test/jdk/java/util/stream/GatherersTest.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.*; +import java.util.stream.Gatherer; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; + +/** + * @test + * @summary Testing the built-in Gatherer implementations and their contracts + * @enablePreview + * @run junit GatherersTest + */ + +public class GatherersTest { + + record Config(int streamSize, boolean parallel) { + Stream stream() { + return wrapStream(Stream.iterate(1, i -> i + 1).limit(streamSize)); + } + + Stream wrapStream(Stream stream) { + stream = parallel ? stream.parallel() : stream.sequential(); + return stream; + } + } + + final static Stream configurations() { + return Stream.of(0,1,10,33,99,9999) + .flatMap(size -> + Stream.of(false, true) + .map(parallel -> + new Config(size, parallel)) + ); + } + + final class TestException extends RuntimeException { + TestException(String message) { + super(message); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testFixedWindowAPIandContract(Config config) { + // Groups must be greater than 0 + assertThrows(IllegalArgumentException.class, () -> Gatherers.windowFixed(0)); + + final var streamSize = config.streamSize(); + + // We're already covering less-than-one scenarios above + if (streamSize > 0) { + //Test creating a window of the same size as the stream + { + final var result = config.stream() + .gather(Gatherers.windowFixed(streamSize)) + .toList(); + assertEquals(1, result.size()); + assertEquals(config.stream().toList(), result.get(0)); + } + + //Test nulls as elements + { + assertEquals( + config.stream() + .map(n -> Arrays.asList(null, null)) + .toList(), + config.stream() + .flatMap(n -> Stream.of(null, null)) + .gather(Gatherers.windowFixed(2)) + .toList()); + } + + // Test unmodifiability of windows + { + var window = config.stream() + .gather(Gatherers.windowFixed(1)) + .findFirst() + .get(); + assertThrows(UnsupportedOperationException.class, + () -> window.add(2)); + } + } + + + // Tests that the layout of the returned data is as expected + for (var windowSize : List.of(1, 2, 3, 10)) { + final var expectLastWindowSize = streamSize % windowSize == 0 ? windowSize : streamSize % windowSize; + final var expectedSize = (streamSize / windowSize) + ((streamSize % windowSize == 0) ? 0 : 1); + + final var expected = config.stream().toList().iterator(); + + final var result = config.stream() + .gather(Gatherers.windowFixed(windowSize)) + .toList(); + + int currentWindow = 0; + for (var window : result) { + ++currentWindow; + assertEquals(currentWindow < expectedSize ? windowSize : expectLastWindowSize, window.size()); + for (var element : window) + assertEquals(expected.next(), element); + } + + assertEquals(expectedSize, currentWindow); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testSlidingAPIandContract(Config config) { + // Groups must be greater than 0 + assertThrows(IllegalArgumentException.class, () -> Gatherers.windowSliding(0)); + + final var streamSize = config.streamSize(); + + // We're already covering less-than-one scenarios above + if (streamSize > 0) { + //Test greating a window larger than the size of the stream + { + final var result = config.stream() + .gather(Gatherers.windowSliding(streamSize + 1)) + .toList(); + assertEquals(1, result.size()); + assertEquals(config.stream().toList(), result.get(0)); + } + + //Test nulls as elements + { + assertEquals( + List.of( + Arrays.asList(null, null), + Arrays.asList(null, null) + ), + config.wrapStream(Stream.of(null, null, null)) + .gather(Gatherers.windowSliding(2)) + .toList()); + } + + // Test unmodifiability of windows + { + var window = config.stream() + .gather(Gatherers.windowSliding(1)) + .findFirst() + .get(); + assertThrows(UnsupportedOperationException.class, + () -> window.add(2)); + } + } + + // Tests that the layout of the returned data is as expected + for (var windowSize : List.of(1, 2, 3, 10)) { + final var expectLastWindowSize = streamSize < windowSize ? streamSize : windowSize; + final var expectedNumberOfWindows = streamSize == 0 ? 0 : Math.max(1, 1 + streamSize - windowSize); + + int expectedElement = 0; + int currentWindow = 0; + + final var result = config.stream() + .gather(Gatherers.windowSliding(windowSize)) + .toList(); + + for (var window : result) { + ++currentWindow; + assertEquals(currentWindow < expectedNumberOfWindows ? windowSize : expectLastWindowSize, window.size()); + for (var element : window) { + assertEquals(++expectedElement, element.intValue()); + } + // rewind for the sliding motion + expectedElement -= (window.size() - 1); + } + + assertEquals(expectedNumberOfWindows, currentWindow); + } + } + + @ParameterizedTest + @MethodSource("configurations") + public void testFoldAPIandContract(Config config) { + // Verify prereqs + assertThrows(NullPointerException.class, () -> Gatherers.fold(null, (state, next) -> state)); + assertThrows(NullPointerException.class, () -> Gatherers.fold(() -> "", null)); + + final var expectedResult = List.of( + config.stream() + .sequential() + .reduce("", (acc, next) -> acc + next, (l,r) -> { throw new IllegalStateException(); }) + ); + + final var result = config.stream() + .gather(Gatherers.fold(() -> "", (acc, next) -> acc + next)) + .toList(); + + assertEquals(expectedResult, result); + } + + @ParameterizedTest + @MethodSource("configurations") + public void testMapConcurrentAPIandContract(Config config) throws InterruptedException { + // Verify prereqs + assertThrows(IllegalArgumentException.class, () -> Gatherers.mapConcurrent(0, s -> s)); + assertThrows(NullPointerException.class, () -> Gatherers.mapConcurrent(2, null)); + + // Test exception during processing + { + final var stream = config.parallel() ? Stream.of(1).parallel() : Stream.of(1); + + assertThrows(RuntimeException.class, + () -> stream.gather(Gatherers.mapConcurrent(2, x -> { + throw new RuntimeException(); + })).toList()); + } + + // Test cancellation after exception during processing + if (config.streamSize > 2) { // We need streams of a minimum size to test this + final var firstLatch = new CountDownLatch(1); + final var secondLatch = new CountDownLatch(1); + final var cancellationLatch = new CountDownLatch(config.streamSize - 2); // all but two will get cancelled + + try { + config.stream() + .gather( + Gatherers.mapConcurrent(config.streamSize(), i -> { + switch (i) { + case 1 -> { + try { + firstLatch.await(); // the first waits for the last element to start + } catch (InterruptedException ie) { + throw new IllegalStateException(ie); + } + throw new TestException("expected"); + } + + case Integer n when n == config.streamSize - 1 -> { // last element + firstLatch.countDown(); // ensure that the first element can now proceed + } + + default -> { + try { + secondLatch.await(); // These should all get interrupted + } catch (InterruptedException ie) { + cancellationLatch.countDown(); // used to ensure that they all were interrupted + } + } + } + + return i; + }) + ) + .toList(); + fail("This should not be reached"); + } catch (RuntimeException re) { + assertSame(TestException.class, re.getClass()); + assertEquals("expected", re.getMessage()); + cancellationLatch.await(); + return; + } + + fail("This should not be reached"); + } + + // Test cancellation during short-circuiting + if (config.streamSize > 2) { + final var firstLatch = new CountDownLatch(1); + final var secondLatch = new CountDownLatch(1); + final var cancellationLatch = new CountDownLatch(config.streamSize - 2); // all but two will get cancelled + + final var result = + config.stream() + .gather( + Gatherers.mapConcurrent(config.streamSize(), i -> { + switch (i) { + case 1 -> { + try { + firstLatch.await(); // the first waits for the last element to start + } catch (InterruptedException ie) { + throw new IllegalStateException(ie); + } + } + + case Integer n when n == config.streamSize - 1 -> { // last element + firstLatch.countDown(); // ensure that the first element can now proceed + } + + default -> { + try { + secondLatch.await(); // These should all get interrupted + } catch (InterruptedException ie) { + cancellationLatch.countDown(); // used to ensure that they all were interrupted + } + } + } + + return i; + }) + ) + .limit(2) + .toList(); + cancellationLatch.await(); // If this hangs, then we didn't cancel and interrupt the tasks + assertEquals(List.of(1,2), result); + } + + for (var concurrency : List.of(1, 2, 3, 10, 1000)) { + // Test normal operation + { + final var expectedResult = config.stream() + .map(x -> x * x) + .toList(); + + final var result = config.stream() + .gather(Gatherers.mapConcurrent(concurrency, x -> x * x)) + .toList(); + + assertEquals(expectedResult, result); + } + + // Test short-circuiting + { + final var limitTo = Math.max(config.streamSize() / 2, 1); + + final var expectedResult = config.stream() + .map(x -> x * x) + .limit(limitTo) + .toList(); + + final var result = config.stream() + .gather(Gatherers.mapConcurrent(concurrency, x -> x * x)) + .limit(limitTo) + .toList(); + + assertEquals(expectedResult, result); + } + } + } +} diff --git a/test/jdk/lib/testlibrary/bootlib/java.base/java/util/stream/DefaultMethodStreams.java b/test/jdk/lib/testlibrary/bootlib/java.base/java/util/stream/DefaultMethodStreams.java index d16603e9cf8..bc7c3bc471d 100644 --- a/test/jdk/lib/testlibrary/bootlib/java.base/java/util/stream/DefaultMethodStreams.java +++ b/test/jdk/lib/testlibrary/bootlib/java.base/java/util/stream/DefaultMethodStreams.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/BenchmarkGathererImpls.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/BenchmarkGathererImpls.java new file mode 100644 index 00000000000..623500f16c2 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/BenchmarkGathererImpls.java @@ -0,0 +1,272 @@ +package org.openjdk.bench.java.util.stream.ops.ref; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Gatherer; +import java.util.stream.Stream; + +// Utility Gatherer and Collector implementations used by Gatherer micro-benchmarks +public final class BenchmarkGathererImpls { + + public static Gatherer filter(Predicate predicate) { + return new FilteringGatherer<>(predicate); + } + + public final static Gatherer map(Function mapper) { + return new MappingGatherer<>(Objects.requireNonNull(mapper)); + } + + public static Gatherer reduce(BinaryOperator reduce) { + Objects.requireNonNull(reduce); + return new ReducingGatherer<>(reduce); + } + + public final static Gatherer takeWhile(Predicate predicate) { + return new TakeWhileGatherer<>(Objects.requireNonNull(predicate)); + } + + @SuppressWarnings("unchecked") + public final static Collector> findFirst() { + return (Collector>)FIND_FIRST; + } + + @SuppressWarnings("unchecked") + public final static Collector> findLast() { + return (Collector>)FIND_LAST; + } + + @SuppressWarnings("rawtypes") + private final static Collector FIND_FIRST = + Collector.,Optional>of( + () -> new Box<>(), + (b,e) -> { + if (!b.hasValue) { + b.value = e; + b.hasValue = true; + } + }, + (l,r) -> l.hasValue ? l : r, + b -> b.hasValue ? Optional.of(b.value) : Optional.empty() + ); + + @SuppressWarnings("rawtypes") + private final static Collector FIND_LAST = + Collector.,Optional>of( + () -> new Box<>(), + (b,e) -> { + b.value = e; + if (!b.hasValue) + b.hasValue = true; + }, + (l,r) -> r.hasValue ? r : l, + b -> b.hasValue ? Optional.of(b.value) : Optional.empty() + ); + + public final static Gatherer flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + + class FlatMappingGatherer implements Gatherer, Gatherer.Integrator, BinaryOperator { + @Override public Integrator integrator() { return this; } + + // Ideal encoding, but performance-wise suboptimal due to cost of allMatch--about factor-10 worse. + /*@Override public boolean integrate(Void state, T element, Gatherer.Downstream downstream) { + try(Stream s = mapper.apply(element)) { + return s != null ? s.sequential().allMatch(downstream::flush) : true; + } + }*/ + + //The version below performs better, but is not nice to maintain or explain. + + private final static RuntimeException SHORT_CIRCUIT = new RuntimeException() { + @Override public synchronized Throwable fillInStackTrace() { return this; } + }; + + @Override public boolean integrate(Void state, T element, Gatherer.Downstream downstream) { + try (Stream s = mapper.apply(element)) { + if (s != null) { + s.sequential().spliterator().forEachRemaining(e -> { + if (!downstream.push(e)) throw SHORT_CIRCUIT; + }); + } + return true; + } catch (RuntimeException e) { + if (e == SHORT_CIRCUIT) + return false; + + throw e; // Rethrow anything else + } + } + + @Override public BinaryOperator combiner() { return this; } + @Override public Void apply(Void unused, Void unused2) { return unused; } + } + + return new FlatMappingGatherer(); + } + + final static class MappingGatherer implements Gatherer, Gatherer.Integrator.Greedy, BinaryOperator { + final Function mapper; + + MappingGatherer(Function mapper) { this.mapper = mapper; } + + @Override public Integrator integrator() { return this; } + @Override public BinaryOperator combiner() { return this; } + @Override public Void apply(Void left, Void right) { return left; } + + @Override + public Gatherer andThen(Gatherer that) { + if (that.getClass() == MappingGatherer.class) { // Implicit null-check of that + @SuppressWarnings("unchecked") + var thatMapper = ((MappingGatherer)that).mapper; + return new MappingGatherer<>(this.mapper.andThen(thatMapper)); + } else + return Gatherer.super.andThen(that); + } + + @Override + public boolean integrate(Void state, T element, Gatherer.Downstream downstream) { + return downstream.push(mapper.apply(element)); + } + } + + + final static class FilteringGatherer implements Gatherer, Gatherer.Integrator.Greedy, BinaryOperator { + final Predicate predicate; + + protected FilteringGatherer(Predicate predicate) { this.predicate = predicate; } + + @Override public Integrator integrator() { return this; } + @Override public BinaryOperator combiner() { return this; } + + @Override public Void apply(Void left, Void right) { return left; } + + @Override + public boolean integrate(Void state, TR element, Gatherer.Downstream downstream) { + return predicate.test(element) ? downstream.push(element) : true; + } + + @Override + @SuppressWarnings("unchecked") + public Gatherer andThen(Gatherer that) { + if (that.getClass() == FilteringGatherer.class) { + var first = predicate; + var second = ((FilteringGatherer) that).predicate; + return (Gatherer) new FilteringGatherer(e -> first.test(e) && second.test(e)); + } else if (that.getClass() == MappingGatherer.class) { + final var thatMapper = (MappingGatherer)that; + return new FilteringMappingGatherer<>(predicate, thatMapper.mapper); + } else if (that.getClass() == FilteringMappingGatherer.class) { + var first = predicate; + var thatFilterMapper = ((FilteringMappingGatherer) that); + var second = thatFilterMapper.predicate; + return new FilteringMappingGatherer<>(e -> first.test(e) && second.test(e), thatFilterMapper.mapper); + } else + return Gatherer.super.andThen(that); + } + } + + final static class FilteringMappingGatherer implements Gatherer, Gatherer.Integrator.Greedy, BinaryOperator { + final Predicate predicate; + final Function mapper; + + FilteringMappingGatherer(Predicate predicate, Function mapper) { + this.predicate = predicate; + this.mapper = mapper; + } + + @Override public Integrator integrator() { return this; } + @Override public BinaryOperator combiner() { return this; } + @Override public Void apply(Void left, Void right) { return left; } + + @Override + public Gatherer andThen(Gatherer that) { + if (that.getClass() == MappingGatherer.class) { // Implicit null-check of that + @SuppressWarnings("unchecked") + var thatMapper = ((MappingGatherer)that).mapper; + return new FilteringMappingGatherer<>(this.predicate, this.mapper.andThen(thatMapper)); + } else + return Gatherer.super.andThen(that); + } + + @Override + public boolean integrate(Void state, T element, Gatherer.Downstream downstream) { + return !predicate.test(element) || downstream.push(mapper.apply(element)); + } + } + + final static class ReducingGatherer implements Gatherer, TR>, + Supplier>, + Gatherer.Integrator.Greedy, TR, TR>, + BinaryOperator>, + BiConsumer, Gatherer.Downstream> { + private final BinaryOperator reduce; + ReducingGatherer(BinaryOperator reduce) { this.reduce = reduce; } + + @Override public Box get() { return new Box<>(); } + + @Override + public boolean integrate(Box state, TR m, Gatherer.Downstream downstream) { + state.value = state.hasValue || !(state.hasValue = true) ? reduce.apply(state.value, m) : m; + return true; + } + + @Override public Box apply(Box left, Box right) { + if (right.hasValue) + integrate(left, right.value, null); + return left; + } + + @Override public void accept(Box box, Gatherer.Downstream downstream) { + if (box.hasValue) + downstream.push(box.value); + } + + @Override public Supplier> initializer() { return this; } + @Override public Integrator, TR, TR> integrator() { return this; } + @Override public BinaryOperator> combiner() { return this; } + @Override public BiConsumer, Gatherer.Downstream> finisher() { return this; } + } + + final static class TakeWhileGatherer implements Gatherer, Gatherer.Integrator, BinaryOperator { + final Predicate predicate; + TakeWhileGatherer(Predicate predicate) { this.predicate = predicate; } + + @Override public Integrator integrator() { return this; } + @Override public BinaryOperator combiner() { return this; } + + @Override public Void apply(Void left, Void right) { return left; } + + @Override public boolean integrate(Void state, TR element, Gatherer.Downstream downstream) { + return predicate.test(element) && downstream.push(element); + } + + @Override + @SuppressWarnings("unchecked") + public final Gatherer andThen(Gatherer that) { + if (that.getClass() == TakeWhileGatherer.class) { + final var thisPredicate = predicate; + final var thatPredicate = ((TakeWhileGatherer)that).predicate; + return (Gatherer)new TakeWhileGatherer(e -> thisPredicate.test(e) && thatPredicate.test(e)); + } + else + return Gatherer.super.andThen(that); + } + } + + final static class Box { + T value; + boolean hasValue; + + Box() {} + } +} \ No newline at end of file diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRPar.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRPar.java new file mode 100644 index 00000000000..476299df76e --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRPar.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.Arrays; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.filter; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for filter+map+reduce operations implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherFMRPar { + + @Param({"10","100","1000000"}) + private int size; + + private Function squared; + private Predicate evens; + + private Gatherer gathered; + private Gatherer ga_map_squared; + private Gatherer ga_filter_evens; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + squared = new Function() { @Override public Long apply(Long l) { return l*l; } }; + evens = new Predicate() { @Override public boolean test(Long l) { + return l % 2 == 0; + } }; + + ga_map_squared = map(squared); + ga_filter_evens = filter(evens); + + gathered = ga_filter_evens.andThen(ga_map_squared); + } + + @Benchmark + public long par_fmr_baseline() { + return Arrays.stream(cachedInputArray) + .parallel() + .filter(evens) + .map(squared) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_fmr_gather() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(filter(evens)) + .gather(map(squared)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_fmr_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(ga_filter_evens) + .gather(ga_map_squared) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_fmr_gather_composed() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(filter(evens).andThen(map(squared))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_fmr_gather_composed_preallocated() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(filter(evens).andThen(map(squared))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_fmr_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(gathered) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRSeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRSeq.java new file mode 100644 index 00000000000..05e48e16d07 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFMRSeq.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.Arrays; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.filter; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for filter+map+reduce operations implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherFMRSeq { + + @Param({"10","100","1000000"}) + private int size; + + private Function squared; + private Predicate evens; + + private Gatherer gathered; + private Gatherer ga_map_squared; + private Gatherer ga_filter_evens; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + squared = new Function() { @Override public Long apply(Long l) { return l*l; } }; + evens = new Predicate() { @Override public boolean test(Long l) { + return l % 2 == 0; + } }; + + ga_map_squared = map(squared); + ga_filter_evens = filter(evens); + + gathered = ga_filter_evens.andThen(ga_map_squared); + } + + @Benchmark + public long seq_fmr_baseline() { + return Arrays.stream(cachedInputArray) + .filter(evens) + .map(squared) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_fmr_gather() { + return Arrays.stream(cachedInputArray) + .gather(filter(evens)) + .gather(map(squared)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_fmr_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(ga_filter_evens) + .gather(ga_map_squared) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_fmr_gather_composed() { + return Arrays.stream(cachedInputArray) + .gather(filter(evens).andThen(map(squared))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_fmr_gather_composed_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(filter(evens).andThen(map(squared))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_fmr_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .gather(gathered) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapInfinitySeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapInfinitySeq.java new file mode 100644 index 00000000000..09c38f83484 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapInfinitySeq.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.Arrays; +import java.util.stream.Stream; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.flatMap; + +/** + * Benchmark for map() operation implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherFlatMapInfinitySeq { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + + @Param({"10", "100", "1000"}) + private int size; + + private Function> funInf; + + private Long[] cachedInputArray; + + private Gatherer gather_flatMap_inf; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + funInf = new Function>() { @Override public Stream apply(Long l) { + return Stream.generate(() -> l); + } }; + + gather_flatMap_inf = flatMap(funInf); + } + + @Benchmark + public long seq_invoke_baseline() { + return Arrays.stream(cachedInputArray) + .flatMap(funInf) + .limit(size * 5) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather() { + return Arrays.stream(cachedInputArray) + .gather(flatMap(funInf)) + .limit(size * 5) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(gather_flatMap_inf) + .limit(size * 5) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapSeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapSeq.java new file mode 100644 index 00000000000..a13ee01a851 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherFlatMapSeq.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.Arrays; +import java.util.stream.Stream; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.flatMap; + +/** + * Benchmark for map() operation implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherFlatMapSeq { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + + @Param({"10", "100", "1000"}) + private int size; + + private Function> fun; + + private Gatherer gather_flatMap; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + fun = new Function>() { @Override public Stream apply(Long l) { + return Arrays.stream(cachedInputArray); + } }; + + gather_flatMap = flatMap(fun); + } + + @Benchmark + public long seq_invoke_baseline() { + return Arrays.stream(cachedInputArray) + .flatMap(fun) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather() { + return Arrays.stream(cachedInputArray) + .gather(flatMap(fun)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(gather_flatMap) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapPar.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapPar.java new file mode 100644 index 00000000000..62c2d03b1b1 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapPar.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for map() operation implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherMapPar { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + @Param({"10","100","1000000"}) + private int size; + + private Function m1, m2, m3; + + private Gatherer gather_m1, gather_m2, gather_m3, gather_all, gather_m1_111; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + m1 = new Function() { @Override public Long apply(Long l) { return l*2; } }; + m2 = new Function() { @Override public Long apply(Long l) { return l*2; } }; + m3 = new Function() { @Override public Long apply(Long l) { return l*2; } }; + gather_m1 = map(m1); + gather_m2 = map(m2); + gather_m3 = map(m3); + gather_all = gather_m1.andThen(gather_m2.andThen(gather_m3)); + gather_m1_111 = gather_m1.andThen(gather_m1.andThen(gather_m1)); + } + + @Benchmark + public long par_invoke_baseline() { + return Arrays.stream(cachedInputArray).parallel() + .map(m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_invoke_gather() { + return Arrays.stream(cachedInputArray).parallel() + .gather(map(m1)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray).parallel() + .gather(gather_m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_111_baseline() { + return Arrays.stream(cachedInputArray).parallel() + .map(m1) + .map(m1) + .map(m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_111_gather_separate() { + return Arrays.stream(cachedInputArray).parallel() + .gather(map(m1)) + .gather(map(m1)) + .gather(map(m1)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_111_gather_composed() { + return Arrays.stream(cachedInputArray).parallel() + .gather(gather_m1.andThen(gather_m1).andThen(gather_m1)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_111_gather_precomposed() { + return Arrays.stream(cachedInputArray).parallel() + .gather(gather_m1_111) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_123_baseline() { + return Arrays.stream(cachedInputArray).parallel() + .map(m1) + .map(m2) + .map(m3) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_123_gather_separate() { + return Arrays.stream(cachedInputArray).parallel() + .gather(map(m1)) + .gather(map(m2)) + .gather(map(m3)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_123_gather_composed() { + return Arrays.stream(cachedInputArray).parallel() + .gather(map(m1).andThen(map(m2).andThen(map(m3)))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_chain_123_gather_precomposed() { + return Arrays.stream(cachedInputArray).parallel() + .gather(gather_all) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapSeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapSeq.java new file mode 100644 index 00000000000..1e842c0c09e --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMapSeq.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.Arrays; +import java.util.stream.Collector; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for map() operation implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherMapSeq { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + + @Param({"10", "100", "1000000"}) + private int size; + + private Function m1, m2, m3; + + private Gatherer gather_m1, gather_m2, gather_m3, gather_all, gather_m1_111; + + private Long[] cachedInputArray; + + private final static Collector accumulate = + Collector.of(LongAccumulator::new, + LongAccumulator::add, + (l,r) -> { l.merge(r); return l; }, + LongAccumulator::get); + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + m1 = new Function() { @Override public Long apply(Long l) { + return l*2; + } }; + m2 = new Function() { @Override public Long apply(Long l) { + return l*2; + } }; + m3 = new Function() { @Override public Long apply(Long l) { + return l*2; + } }; + gather_m1 = map(m1); + gather_m2 = map(m2); + gather_m3 = map(m3); + gather_all = gather_m1.andThen(gather_m2.andThen(gather_m3)); + gather_m1_111 = gather_m1.andThen(gather_m1.andThen(gather_m1)); + } + + @Benchmark + public long seq_invoke_baseline() { + return Arrays.stream(cachedInputArray) + .map(m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather() { + return Arrays.stream(cachedInputArray) + .gather(map(m1)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(gather_m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_111_baseline() { + return Arrays.stream(cachedInputArray) + .map(m1) + .map(m1) + .map(m1) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_111_gather_separate() { + return Arrays.stream(cachedInputArray) + .gather(map(m1)) + .gather(map(m1)) + .gather(map(m1)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_111_gather_composed() { + return Arrays.stream(cachedInputArray) + .gather(map(m1).andThen(map(m1)).andThen(map(m1))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_111_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .gather(gather_m1_111) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_123_baseline() { + return Arrays.stream(cachedInputArray) + .map(m1) + .map(m2) + .map(m3) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_123_gather_separate() { + return Arrays.stream(cachedInputArray) + .gather(map(m1)) + .gather(map(m2)) + .gather(map(m3)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_123_gather_composed() { + return Arrays.stream(cachedInputArray) + .gather(map(m1).andThen(map(m2)).andThen(map(m3))) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_chain_123_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .gather(gather_all) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscPar.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscPar.java new file mode 100644 index 00000000000..578e470ec5c --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscPar.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.Arrays; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.filter; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for misc operations implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherMiscPar { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + + @Param({"10","100","1000000"}) + private int size; + + private Function timesTwo, halved; + private Predicate evens, odds; + + private Gatherer gathered; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + timesTwo = new Function() { @Override public Long apply(Long l) { + return l*2; + } }; + halved = new Function() { @Override public Long apply(Long l) { return l/2; } }; + + evens = new Predicate() { @Override public boolean test(Long l) { + return l % 2 == 0; + } }; + odds = new Predicate() { @Override public boolean test(Long l) { + return l % 2 != 0; + } }; + + gathered = filter(odds) + .andThen(map(timesTwo)) + .andThen(map(halved)) + .andThen(filter(evens)); + } + + @Benchmark + public long par_misc_baseline() { + return Arrays.stream(cachedInputArray) + .parallel() + .filter(odds) + .map(timesTwo) + .map(halved) + .filter(evens) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_misc_gather() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(filter(odds)) + .gather(map(timesTwo)) + .gather(map(halved)) + .gather(filter(evens)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_misc_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .parallel() + .gather(gathered) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscSeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscSeq.java new file mode 100644 index 00000000000..0d7d9bdef9f --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherMiscSeq.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.filter; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.findLast; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.map; + +/** + * Benchmark for misc operations implemented as Gatherer, with the default map implementation of Stream as baseline. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherMiscSeq { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + * - the result of applying consecutive operations is the same, in order to have the same number of elements in sink + */ + + @Param({"10","100","1000000"}) + private int size; + + private Function timesTwo, squared; + private Predicate evens, odds; + + private Gatherer gathered; + private Gatherer ga_filter_odds; + private Gatherer ga_map_timesTwo; + private Gatherer ga_map_squared; + private Gatherer ga_filter_evens; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + timesTwo = new Function() { @Override public Long apply(Long l) { + return l*2; + } }; + squared = new Function() { @Override public Long apply(Long l) { return l*l; } }; + + evens = new Predicate() { @Override public boolean test(Long l) { + return l % 2 == 0; + } }; + odds = new Predicate() { @Override public boolean test(Long l) { + return l % 2 != 0; + } }; + + ga_filter_odds = filter(odds); + ga_map_timesTwo = map(timesTwo); + ga_map_squared = map(squared); + ga_filter_evens = filter(evens); + + gathered = ga_filter_odds.andThen(ga_map_timesTwo).andThen(ga_map_squared).andThen(ga_filter_evens); + } + + @Benchmark + public long seq_misc_baseline() { + return Arrays.stream(cachedInputArray) + .filter(odds) + .map(timesTwo) + .map(squared) + .filter(evens) + .collect(findLast()).get(); + } + + @Benchmark + public long seq_misc_gather() { + return Arrays.stream(cachedInputArray) + .gather(filter(odds)) + .gather(map(timesTwo)) + .gather(map(squared)) + .gather(filter(evens)) + .collect(findLast()).get(); + } + + @Benchmark + public long seq_misc_gather_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(ga_filter_odds) + .gather(ga_map_timesTwo) + .gather(ga_map_squared) + .gather(ga_filter_evens) + .collect(findLast()).get(); + } + + @Benchmark + public long seq_misc_gather_composed() { + return Arrays.stream(cachedInputArray) + .gather(filter(odds) + .andThen(map(timesTwo)) + .andThen(map(squared)) + .andThen(filter(evens)) + ) + .collect(findLast()).get(); + } + + @Benchmark + public long seq_misc_gather_composed_preallocated() { + return Arrays.stream(cachedInputArray) + .gather(ga_filter_odds + .andThen(ga_map_timesTwo) + .andThen(ga_map_squared) + .andThen(ga_filter_evens) + ) + .collect(findLast()).get(); + } + + @Benchmark + public long seq_misc_gather_precomposed() { + return Arrays.stream(cachedInputArray) + .gather(gathered) + .collect(findLast()).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReducePar.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReducePar.java new file mode 100644 index 00000000000..2f2d0b06bd7 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReducePar.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.BinaryOperator; +import java.util.Arrays; +import java.util.stream.Collector; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.findFirst; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.reduce; + +/** + * Benchmark for comparing the built-in reduce() operation with the Gatherer-based reduce-operation. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherReducePar { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + */ + + @Param({"100000"}) + private int size; + + private BinaryOperator op1; + + private Gatherer gather_op1; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + op1 = new BinaryOperator() { + @Override public Long apply(Long l, Long r) { + return (l < r) ? r : l; + } + }; + } + + @Benchmark + public long par_invoke_baseline() { + return Arrays.stream(cachedInputArray).parallel().reduce(op1).get(); + } + + @Benchmark + public long par_invoke_gather() { + return Arrays.stream(cachedInputArray).parallel().gather(reduce(op1)).collect(findFirst()).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReduceSeq.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReduceSeq.java new file mode 100644 index 00000000000..7d8540d0ed4 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherReduceSeq.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.BinaryOperator; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Gatherer; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.findFirst; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.reduce; + +/** + * Benchmark for comparing the built-in reduce() operation with the Gatherer-based reduce-operation. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherReduceSeq { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + */ + + @Param({"100", "100000"}) + private int size; + + private BinaryOperator op1; + + private Gatherer gather_op1; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + op1 = new BinaryOperator() { + @Override + public Long apply(Long l, Long r) { + return (l < r) ? r : l; + } + }; + + gather_op1 = reduce(op1); + } + + @Benchmark + public long seq_invoke_baseline() { + return Arrays.stream(cachedInputArray).reduce(op1).get(); + } + + @Benchmark + public long seq_invoke_gather() { + return Arrays.stream(cachedInputArray).gather(reduce(op1)).collect(findFirst()).get(); + } + + @Benchmark + public long seq_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray).gather(gather_op1).collect(findFirst()).get(); + } +} diff --git a/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherWhileOrdered.java b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherWhileOrdered.java new file mode 100644 index 00000000000..6e9e5fec1fa --- /dev/null +++ b/test/micro/org/openjdk/bench/java/util/stream/ops/ref/GatherWhileOrdered.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.util.stream.ops.ref; + +import org.openjdk.bench.java.util.stream.ops.LongAccumulator; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +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; + +import java.util.concurrent.TimeUnit; +import java.util.function.BinaryOperator; +import java.util.function.Predicate; +import java.util.Objects; +import java.util.Arrays; +import java.util.stream.Gatherer; +import static org.openjdk.bench.java.util.stream.ops.ref.BenchmarkGathererImpls.takeWhile; + +/** + * Benchmark for comparing the built-in takeWhile-operation with the Gatherer-based takeWhile-operation for ordered streams. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 7, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(jvmArgsAppend = "--enable-preview", value = 1) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class GatherWhileOrdered { + + /** + * Implementation notes: + * - parallel version requires thread-safe sink, we use the same for sequential version for better comparison + * - operations are explicit inner classes to untangle unwanted lambda effects + */ + + @Param("100000") + private int size; + + @Param({"0", "49999", "99999"}) + private int find; + + private Predicate predicate; + + private Gatherer gather_takeWhile; + + private Long[] cachedInputArray; + + @Setup + public void setup() { + final int limit = find; + + cachedInputArray = new Long[size]; + for(int i = 0;i < size;++i) + cachedInputArray[i] = Long.valueOf(i); + + predicate = new Predicate() { + @Override + public boolean test(Long v) { + return v < limit; + } + }; + + gather_takeWhile = takeWhile(predicate); + } + + @Benchmark + public long seq_invoke_baseline() { + return Arrays.stream(cachedInputArray).takeWhile(predicate) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather() { + return Arrays.stream(cachedInputArray).gather(takeWhile(predicate)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long seq_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray).gather(gather_takeWhile) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_invoke_baseline() { + return Arrays.stream(cachedInputArray).parallel().takeWhile(predicate) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_invoke_gather() { + return Arrays.stream(cachedInputArray).parallel().gather(takeWhile(predicate)) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } + + @Benchmark + public long par_invoke_gather_preallocated() { + return Arrays.stream(cachedInputArray).parallel().gather(gather_takeWhile) + .collect(LongAccumulator::new, LongAccumulator::add, LongAccumulator::merge).get(); + } +}