From fbc4691bfa11f31601fd89d05da63e689343e214 Mon Sep 17 00:00:00 2001 From: Per Minborg Date: Wed, 30 Apr 2025 16:03:25 +0000 Subject: [PATCH] 8351565: Implement JEP 502: Stable Values (Preview) Co-authored-by: Maurizio Cimadamore Reviewed-by: vklang, jvernee, alanb, liach --- .../share/classes/java/lang/StableValue.java | 757 ++++++++++++++++++ .../share/classes/java/util/Collection.java | 2 +- .../java/util/ImmutableCollections.java | 275 ++++++- .../access/JavaUtilCollectionAccess.java | 8 +- .../jdk/internal/javac/PreviewFeature.java | 4 +- .../lang/stable/StableEnumFunction.java | 117 +++ .../internal/lang/stable/StableFunction.java | 83 ++ .../lang/stable/StableIntFunction.java | 83 ++ .../internal/lang/stable/StableSupplier.java | 69 ++ .../jdk/internal/lang/stable/StableUtil.java | 105 +++ .../internal/lang/stable/StableValueImpl.java | 218 +++++ .../lang/StableValue/StableFunctionTest.java | 237 ++++++ .../StableValue/StableIntFunctionTest.java | 109 +++ .../java/lang/StableValue/StableListTest.java | 436 ++++++++++ .../java/lang/StableValue/StableMapTest.java | 357 +++++++++ .../lang/StableValue/StableSupplierTest.java | 104 +++ .../java/lang/StableValue/StableTestUtil.java | 120 +++ .../StableValue/StableValueFactoriesTest.java | 43 + .../lang/StableValue/StableValueTest.java | 389 +++++++++ .../StableValuesSafePublicationTest.java | 179 +++++ .../StableValue/TrustedFieldTypeTest.java | 124 +++ test/jdk/java/util/Collection/MOAT.java | 19 +- .../lang/stable/StableFunctionBenchmark.java | 105 +++ .../stable/StableFunctionSingleBenchmark.java | 88 ++ .../stable/StableIntFunctionBenchmark.java | 102 +++ .../StableIntFunctionSingleBenchmark.java | 84 ++ .../stable/StableMethodHandleBenchmark.java | 136 ++++ .../lang/stable/StableSupplierBenchmark.java | 94 +++ .../lang/stable/StableValueBenchmark.java | 196 +++++ .../lang/stable/VarHandleHolderBenchmark.java | 174 ++++ 30 files changed, 4806 insertions(+), 11 deletions(-) create mode 100644 src/java.base/share/classes/java/lang/StableValue.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java create mode 100644 src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java create mode 100644 test/jdk/java/lang/StableValue/StableFunctionTest.java create mode 100644 test/jdk/java/lang/StableValue/StableIntFunctionTest.java create mode 100644 test/jdk/java/lang/StableValue/StableListTest.java create mode 100644 test/jdk/java/lang/StableValue/StableMapTest.java create mode 100644 test/jdk/java/lang/StableValue/StableSupplierTest.java create mode 100644 test/jdk/java/lang/StableValue/StableTestUtil.java create mode 100644 test/jdk/java/lang/StableValue/StableValueFactoriesTest.java create mode 100644 test/jdk/java/lang/StableValue/StableValueTest.java create mode 100644 test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java create mode 100644 test/jdk/java/lang/StableValue/TrustedFieldTypeTest.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableFunctionBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableFunctionSingleBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionSingleBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableMethodHandleBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableSupplierBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/StableValueBenchmark.java create mode 100644 test/micro/org/openjdk/bench/java/lang/stable/VarHandleHolderBenchmark.java diff --git a/src/java.base/share/classes/java/lang/StableValue.java b/src/java.base/share/classes/java/lang/StableValue.java new file mode 100644 index 00000000000..b12d6f5e921 --- /dev/null +++ b/src/java.base/share/classes/java/lang/StableValue.java @@ -0,0 +1,757 @@ +/* + * Copyright (c) 2025, 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.lang; + +import jdk.internal.access.SharedSecrets; +import jdk.internal.javac.PreviewFeature; +import jdk.internal.lang.stable.StableEnumFunction; +import jdk.internal.lang.stable.StableFunction; +import jdk.internal.lang.stable.StableIntFunction; +import jdk.internal.lang.stable.StableSupplier; +import jdk.internal.lang.stable.StableUtil; +import jdk.internal.lang.stable.StableValueImpl; + +import java.io.Serializable; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.RandomAccess; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +/** + * A stable value is a holder of contents that can be set at most once. + *

+ * A {@code StableValue} is typically created using the factory method + * {@linkplain StableValue#of() {@code StableValue.of()}}. When created this way, + * the stable value is unset, which means it holds no contents. + * Its contents, of type {@code T}, can be set by calling + * {@linkplain #trySet(Object) trySet()}, {@linkplain #setOrThrow(Object) setOrThrow()}, + * or {@linkplain #orElseSet(Supplier) orElseSet()}. Once set, the contents + * can never change and can be retrieved by calling {@linkplain #orElseThrow() orElseThrow()} + * , {@linkplain #orElse(Object) orElse()}, or {@linkplain #orElseSet(Supplier) orElseSet()}. + *

+ * Consider the following example where a stable value field "{@code logger}" is a + * shallowly immutable holder of contents of type {@code Logger} and that is initially + * created as unset, which means it holds no contents. Later in the example, the + * state of the "{@code logger}" field is checked and if it is still unset, + * the contents is set: + * + * {@snippet lang = java: + * public class Component { + * + * // Creates a new unset stable value with no contents + * // @link substring="of" target="#of" : + * private final StableValue logger = StableValue.of(); + * + * private Logger getLogger() { + * if (!logger.isSet()) { + * logger.trySet(Logger.create(Component.class)); + * } + * return logger.orElseThrow(); + * } + * + * public void process() { + * getLogger().info("Process started"); + * // ... + * } + * } + *} + *

+ * If {@code getLogger()} is called from several threads, several instances of + * {@code Logger} might be created. However, the contents can only be set at most once + * meaning the first writer wins. + *

+ * In order to guarantee that, even under races, only one instance of {@code Logger} is + * ever created, the {@linkplain #orElseSet(Supplier) orElseSet()} method can be used + * instead, where the contents are lazily computed, and atomically set, via a + * {@linkplain Supplier supplier}. In the example below, the supplier is provided in the + * form of a lambda expression: + * + * {@snippet lang = java: + * public class Component { + * + * // Creates a new unset stable value with no contents + * // @link substring="of" target="#of" : + * private final StableValue logger = StableValue.of(); + * + * private Logger getLogger() { + * return logger.orElseSet( () -> Logger.create(Component.class) ); + * } + * + * public void process() { + * getLogger().info("Process started"); + * // ... + * } + * } + *} + *

+ * The {@code getLogger()} method calls {@code logger.orElseSet()} on the stable value to + * retrieve its contents. If the stable value is unset, then {@code orElseSet()} + * evaluates the given supplier, and sets the contents to the result; the result is then + * returned to the client. In other words, {@code orElseSet()} guarantees that a + * stable value's contents is set before it returns. + *

+ * Furthermore, {@code orElseSet()} guarantees that out of one or more suppliers provided, + * only at most one is ever evaluated, and that one is only ever evaluated once, + * even when {@code logger.orElseSet()} is invoked concurrently. This property is crucial + * as evaluation of the supplier may have side effects, for example, the call above to + * {@code Logger.create()} may result in storage resources being prepared. + * + *

Stable Functions

+ * Stable values provide the foundation for higher-level functional abstractions. A + * stable supplier is a supplier that computes a value and then caches it into + * a backing stable value storage for subsequent use. A stable supplier is created via the + * {@linkplain StableValue#supplier(Supplier) StableValue.supplier()} factory, by + * providing an underlying {@linkplain Supplier} which is invoked when the stable supplier + * is first accessed: + * + * {@snippet lang = java: + * public class Component { + * + * private final Supplier logger = + * // @link substring="supplier" target="#supplier(Supplier)" : + * StableValue.supplier( () -> Logger.getLogger(Component.class) ); + * + * public void process() { + * logger.get().info("Process started"); + * // ... + * } + * } + *} + * A stable supplier encapsulates access to its backing stable value storage. This means + * that code inside {@code Component} can obtain the logger object directly from the + * stable supplier, without having to go through an accessor method like {@code getLogger()}. + *

+ * A stable int function is a function that takes an {@code int} parameter and + * uses it to compute a result that is then cached by the backing stable value storage + * for that parameter value. A stable {@link IntFunction} is created via the + * {@linkplain StableValue#intFunction(int, IntFunction) StableValue.intFunction()} + * factory. Upon creation, the input range (i.e. {@code [0, size)}) is specified together + * with an underlying {@linkplain IntFunction} which is invoked at most once per input + * value. In effect, the stable int function will act like a cache for the underlying + * {@linkplain IntFunction}: + * + * {@snippet lang = java: + * final class PowerOf2Util { + * + * private PowerOf2Util() {} + * + * private static final int SIZE = 6; + * private static final IntFunction UNDERLYING_POWER_OF_TWO = + * v -> 1 << v; + * + * private static final IntFunction POWER_OF_TWO = + * // @link substring="intFunction" target="#intFunction(int,IntFunction)" : + * StableValue.intFunction(SIZE, UNDERLYING_POWER_OF_TWO); + * + * public static int powerOfTwo(int a) { + * return POWER_OF_TWO.apply(a); + * } + * } + * + * int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime + * + *} + * The {@code PowerOf2Util.powerOfTwo()} function is a partial function that only + * allows a subset {@code [0, 5]} of the underlying function's {@code UNDERLYING_POWER_OF_TWO} + * input range. + * + *

+ * A stable function is a function that takes a parameter (of type {@code T}) and + * uses it to compute a result (of type {@code R}) that is then cached by the backing + * stable value storage for that parameter value. A stable function is created via the + * {@linkplain StableValue#function(Set, Function) StableValue.function()} factory. + * Upon creation, the input {@linkplain Set} is specified together with an underlying + * {@linkplain Function} which is invoked at most once per input value. In effect, the + * stable function will act like a cache for the underlying {@linkplain Function}: + * + * {@snippet lang = java: + * class Log2Util { + * + * private Log2Util() {} + * + * private static final Set KEYS = + * Set.of(1, 2, 4, 8, 16, 32); + * private static final UnaryOperator UNDERLYING_LOG2 = + * i -> 31 - Integer.numberOfLeadingZeros(i); + * + * private static final Function LOG2 = + * // @link substring="function" target="#function(Set,Function)" : + * StableValue.function(KEYS, UNDERLYING_LOG2); + * + * public static int log2(int a) { + * return LOG2.apply(a); + * } + * + * } + * + * int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime + *} + * + * The {@code Log2Util.log2()} function is a partial function that only allows + * a subset {@code {1, 2, 4, 8, 16, 32}} of the underlying function's + * {@code UNDERLYING_LOG2} input range. + * + *

Stable Collections

+ * Stable values can also be used as backing storage for + * {@linkplain Collection##unmodifiable unmodifiable collections}. A stable list + * is an unmodifiable list, backed by an array of stable values. The stable list elements + * are computed when they are first accessed, using a provided {@linkplain IntFunction}: + * + * {@snippet lang = java: + * final class PowerOf2Util { + * + * private PowerOf2Util() {} + * + * private static final int SIZE = 6; + * private static final IntFunction UNDERLYING_POWER_OF_TWO = + * v -> 1 << v; + * + * private static final List POWER_OF_TWO = + * // @link substring="list" target="#list(int,IntFunction)" : + * StableValue.list(SIZE, UNDERLYING_POWER_OF_TWO); + * + * public static int powerOfTwo(int a) { + * return POWER_OF_TWO.get(a); + * } + * } + * + * int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime + * + * } + *

+ * Similarly, a stable map is an unmodifiable map whose keys are known at + * construction. The stable map values are computed when they are first accessed, + * using a provided {@linkplain Function}: + * + * {@snippet lang = java: + * class Log2Util { + * + * private Log2Util() {} + * + * private static final Set KEYS = + * Set.of(1, 2, 4, 8, 16, 32); + * private static final UnaryOperator UNDERLYING_LOG2 = + * i -> 31 - Integer.numberOfLeadingZeros(i); + * + * private static final Map LOG2 = + * // @link substring="map" target="#map(Set,Function)" : + * StableValue.map(CACHED_KEYS, UNDERLYING_LOG2); + * + * public static int log2(int a) { + * return LOG2.get(a); + * } + * + * } + * + * int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime + * + *} + * + *

Composing stable values

+ * A stable value can depend on other stable values, forming a dependency graph + * that can be lazily computed but where access to individual elements can still be + * performant. In the following example, a single {@code Foo} and a {@code Bar} + * instance (that is dependent on the {@code Foo} instance) are lazily created, both of + * which are held by stable values: + * {@snippet lang = java: + * public final class DependencyUtil { + * + * private DependencyUtil() {} + * + * public static class Foo { + * // ... + * } + * + * public static class Bar { + * public Bar(Foo foo) { + * // ... + * } + * } + * + * private static final Supplier FOO = StableValue.supplier(Foo::new); + * private static final Supplier BAR = StableValue.supplier(() -> new Bar(FOO.get())); + * + * public static Foo foo() { + * return FOO.get(); + * } + * + * public static Bar bar() { + * return BAR.get(); + * } + * + * } + *} + * Calling {@code bar()} will create the {@code Bar} singleton if it is not already + * created. Upon such a creation, the dependent {@code Foo} will first be created if + * the {@code Foo} does not already exist. + *

+ * Another example, which has a more complex dependency graph, is to compute the + * Fibonacci sequence lazily: + * {@snippet lang = java: + * public final class Fibonacci { + * + * private Fibonacci() {} + * + * private static final int MAX_SIZE_INT = 46; + * + * private static final IntFunction FIB = + * StableValue.intFunction(MAX_SIZE_INT, Fibonacci::fib); + * + * public static int fib(int n) { + * return n < 2 + * ? n + * : FIB.apply(n - 1) + FIB.apply(n - 2); + * } + * + * } + *} + * Both {@code FIB} and {@code Fibonacci::fib} recurse into each other. Because the + * stable int function {@code FIB} caches intermediate results, the initial + * computational complexity is reduced from exponential to linear compared to a + * traditional non-caching recursive fibonacci method. Once computed, the VM is free to + * constant-fold expressions like {@code Fibonacci.fib(5)}. + *

+ * The fibonacci example above is a directed acyclic graph (i.e., + * it has no circular dependencies and is therefore a dependency tree): + *{@snippet lang=text : + * + * ___________fib(5)____________ + * / \ + * ____fib(4)____ ____fib(3)____ + * / \ / \ + * fib(3) fib(2) fib(2) fib(1) + * / \ / \ / \ + * fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) + *} + * + * If there are circular dependencies in a dependency graph, a stable value will + * eventually throw an {@linkplain IllegalStateException} upon referencing elements in + * a circularity. + * + *

Thread Safety

+ * The contents of a stable value is guaranteed to be set at most once. If competing + * threads are racing to set a stable value, only one update succeeds, while the other + * updates are blocked until the stable value is set, whereafter the other updates + * observes the stable value is set and leave the stable value unchanged. + *

+ * The at-most-once write operation on a stable value that succeeds + * (e.g. {@linkplain #trySet(Object) trySet()}) + * {@linkplain java.util.concurrent##MemoryVisibility happens-before} + * any successful read operation (e.g. {@linkplain #orElseThrow()}). + * A successful write operation can be either: + *

    + *
  • a {@link #trySet(Object)} that returns {@code true},
  • + *
  • a {@link #setOrThrow(Object)} that does not throw, or
  • + *
  • an {@link #orElseSet(Supplier)} that successfully runs the supplier
  • + *
+ * A successful read operation can be either: + *
    + *
  • a {@link #orElseThrow()} that does not throw,
  • + *
  • a {@link #orElse(Object) orElse(other)} that does not return the {@code other} value
  • + *
  • an {@link #orElseSet(Supplier)} that does not {@code throw}, or
  • + *
  • an {@link #isSet()} that returns {@code true}
  • + *
+ *

+ * The method {@link #orElseSet(Supplier)} guarantees that the provided + * {@linkplain Supplier} is invoked successfully at most once, even under race. + * Invocations of {@link #setOrThrow(Object)} form a total order of zero or more + * exceptional invocations followed by zero (if the contents were already set) or one + * successful invocation. Since stable functions and stable collections are built on top + * of the same principles as {@linkplain StableValue#orElseSet(Supplier) orElseSet()} they + * too are thread safe and guarantee at-most-once-per-input invocation. + * + *

Performance

+ * As the contents of a stable value can never change after it has been set, a JVM + * implementation may, for a set stable value, elide all future reads of that + * stable value, and instead directly use any contents that it has previously observed. + * This is true if the reference to the stable value is a constant (e.g. in cases where + * the stable value itself is stored in a {@code static final} field). Stable functions + * and collections are built on top of StableValue. As such, they might also be eligible + * for the same JVM optimizations as for StableValue. + * + * @implSpec Implementing classes of {@code StableValue} are free to synchronize on + * {@code this} and consequently, it should be avoided to + * (directly or indirectly) synchronize on a {@code StableValue}. Hence, + * synchronizing on {@code this} may lead to deadlock. + *

+ * Except for a {@code StableValue}'s contents itself, + * an {@linkplain #orElse(Object) orElse(other)} parameter, and + * an {@linkplain #equals(Object) equals(obj)} parameter; all + * method parameters must be non-null or a {@link NullPointerException} + * will be thrown. + * + * @implNote A {@code StableValue} is mainly intended to be a non-public field in + * a class and is usually neither exposed directly via accessors nor passed as + * a method parameter. + *

+ * Stable functions and collections make reasonable efforts to provide + * {@link Object#toString()} operations that do not trigger evaluation + * of the internal stable values when called. + * Stable collections have {@link Object#equals(Object)} operations that try + * to minimize evaluation of the internal stable values when called. + *

+ * As objects can be set via stable values but never removed, this can be a + * source of unintended memory leaks. A stable value's contents are + * {@linkplain java.lang.ref##reachability strongly reachable}. + * Be advised that reachable stable values will hold their set contents until + * the stable value itself is collected. + *

+ * A {@code StableValue} that has a type parameter {@code T} that is an array + * type (of arbitrary rank) will only allow the JVM to treat the + * array reference as a stable value but not its components. + * Instead, a {@linkplain #list(int, IntFunction) a stable list} of arbitrary + * depth can be used, which provides stable components. More generally, a + * stable value can hold other stable values of arbitrary depth and still + * provide transitive constantness. + *

+ * Stable values, functions, and collections are not {@link Serializable}. + * + * @param type of the contents + * + * @since 25 + */ +@PreviewFeature(feature = PreviewFeature.Feature.STABLE_VALUES) +public sealed interface StableValue + permits StableValueImpl { + + // Principal methods + + /** + * Tries to set the contents of this StableValue to the provided {@code contents}. + * The contents of this StableValue can only be set once, implying this method only + * returns {@code true} once. + *

+ * When this method returns, the contents of this StableValue is always set. + * + * @return {@code true} if the contents of this StableValue was set to the + * provided {@code contents}, {@code false} otherwise + * @param contents to set + * @throws IllegalStateException if a supplier invoked by {@link #orElseSet(Supplier)} + * recursively attempts to set this stable value by calling this method + * directly or indirectly. + */ + boolean trySet(T contents); + + /** + * {@return the contents if set, otherwise, returns the provided {@code other} value} + * + * @param other to return if the contents is not set + */ + T orElse(T other); + + /** + * {@return the contents if set, otherwise, throws {@code NoSuchElementException}} + * + * @throws NoSuchElementException if no contents is set + */ + T orElseThrow(); + + /** + * {@return {@code true} if the contents is set, {@code false} otherwise} + */ + boolean isSet(); + + /** + * {@return the contents; if unset, first attempts to compute and set the + * contents using the provided {@code supplier}} + *

+ * The provided {@code supplier} is guaranteed to be invoked at most once if it + * completes without throwing an exception. If this method is invoked several times + * with different suppliers, only one of them will be invoked provided it completes + * without throwing an exception. + *

+ * If the supplier throws an (unchecked) exception, the exception is rethrown and no + * contents is set. The most common usage is to construct a new object serving + * as a lazily computed value or memoized result, as in: + * + * {@snippet lang=java: + * Value v = stable.orElseSet(Value::new); + * } + *

+ * When this method returns successfully, the contents is always set. + *

+ * The provided {@code supplier} will only be invoked once even if invoked from + * several threads unless the {@code supplier} throws an exception. + * + * @param supplier to be used for computing the contents, if not previously set + * @throws IllegalStateException if the provided {@code supplier} recursively + * attempts to set this stable value. + */ + T orElseSet(Supplier supplier); + + // Convenience methods + + /** + * Sets the contents of this StableValue to the provided {@code contents}, or, if + * already set, throws {@code IllegalStateException}. + *

+ * When this method returns (or throws an exception), the contents is always set. + * + * @param contents to set + * @throws IllegalStateException if the contents was already set + */ + void setOrThrow(T contents); + + // Object methods + + /** + * {@return {@code true} if {@code this == obj}, {@code false} otherwise} + * + * @param obj to check for equality + */ + boolean equals(Object obj); + + /** + * {@return the {@linkplain System#identityHashCode(Object) identity hash code} of + * {@code this} object} + */ + int hashCode(); + + // Factories + + /** + * {@return a new unset stable value} + *

+ * An unset stable value has no contents. + * + * @param type of the contents + */ + static StableValue of() { + return StableValueImpl.of(); + } + + /** + * {@return a new pre-set stable value with the provided {@code contents}} + * + * @param contents to set + * @param type of the contents + */ + static StableValue of(T contents) { + final StableValue stableValue = StableValue.of(); + stableValue.trySet(contents); + return stableValue; + } + + /** + * {@return a new stable supplier} + *

+ * The returned {@linkplain Supplier supplier} is a caching supplier that records + * the value of the provided {@code underlying} supplier upon being first accessed via + * the returned supplier's {@linkplain Supplier#get() get()} method. + *

+ * The provided {@code underlying} supplier is guaranteed to be successfully invoked + * at most once even in a multi-threaded environment. Competing threads invoking the + * returned supplier's {@linkplain Supplier#get() get()} method when a value is + * already under computation will block until a value is computed or an exception is + * thrown by the computing thread. The computing threads will then observe the newly + * computed value (if any) and will then never execute. + *

+ * If the provided {@code underlying} supplier throws an exception, it is rethrown + * to the initial caller and no contents is recorded. + *

+ * If the provided {@code underlying} supplier recursively calls the returned + * supplier, an {@linkplain IllegalStateException} will be thrown. + * + * @param underlying supplier used to compute a cached value + * @param the type of results supplied by the returned supplier + */ + static Supplier supplier(Supplier underlying) { + Objects.requireNonNull(underlying); + return StableSupplier.of(underlying); + } + + /** + * {@return a new stable {@linkplain IntFunction}} + *

+ * The returned function is a caching function that, for each allowed {@code int} + * input, records the values of the provided {@code underlying} + * function upon being first accessed via the returned function's + * {@linkplain IntFunction#apply(int) apply()} method. If the returned function is + * invoked with an input that is not in the range {@code [0, size)}, an + * {@link IllegalArgumentException} will be thrown. + *

+ * The provided {@code underlying} function is guaranteed to be successfully invoked + * at most once per allowed input, even in a multi-threaded environment. Competing + * threads invoking the returned function's + * {@linkplain IntFunction#apply(int) apply()} method when a value is already under + * computation will block until a value is computed or an exception is thrown by + * the computing thread. + *

+ * If invoking the provided {@code underlying} function throws an exception, it is + * rethrown to the initial caller and no contents is recorded. + *

+ * If the provided {@code underlying} function recursively calls the returned + * function for the same input, an {@linkplain IllegalStateException} will + * be thrown. + * + * @param size the size of the allowed inputs in the continuous + * interval {@code [0, size)} + * @param underlying IntFunction used to compute cached values + * @param the type of results delivered by the returned IntFunction + * @throws IllegalArgumentException if the provided {@code size} is negative. + */ + static IntFunction intFunction(int size, + IntFunction underlying) { + StableUtil.assertSizeNonNegative(size); + Objects.requireNonNull(underlying); + return StableIntFunction.of(size, underlying); + } + + /** + * {@return a new stable {@linkplain Function}} + *

+ * The returned function is a caching function that, for each allowed + * input in the given set of {@code inputs}, records the values of the provided + * {@code underlying} function upon being first accessed via the returned function's + * {@linkplain Function#apply(Object) apply()} method. If the returned function is + * invoked with an input that is not in {@code inputs}, an {@link IllegalArgumentException} + * will be thrown. + *

+ * The provided {@code underlying} function is guaranteed to be successfully invoked + * at most once per allowed input, even in a multi-threaded environment. Competing + * threads invoking the returned function's {@linkplain Function#apply(Object) apply()} + * method when a value is already under computation will block until a value is + * computed or an exception is thrown by the computing thread. + *

+ * If invoking the provided {@code underlying} function throws an exception, it is + * rethrown to the initial caller and no contents is recorded. + *

+ * If the provided {@code underlying} function recursively calls the returned + * function for the same input, an {@linkplain IllegalStateException} will + * be thrown. + * + * @param inputs the set of (non-null) allowed input values + * @param underlying {@code Function} used to compute cached values + * @param the type of the input to the returned Function + * @param the type of results delivered by the returned Function + * @throws NullPointerException if the provided set of {@code inputs} contains a + * {@code null} element. + */ + static Function function(Set inputs, + Function underlying) { + Objects.requireNonNull(inputs); + // Checking that the Set of inputs does not contain a `null` value is made in the + // implementing classes. + Objects.requireNonNull(underlying); + return inputs instanceof EnumSet && !inputs.isEmpty() + ? StableEnumFunction.of(inputs, underlying) + : StableFunction.of(inputs, underlying); + } + + /** + * {@return a new stable list with the provided {@code size}} + *

+ * The returned list is an {@linkplain Collection##unmodifiable unmodifiable} list + * with the provided {@code size}. The list's elements are computed via the + * provided {@code mapper} when they are first accessed + * (e.g. via {@linkplain List#get(int) List::get}). + *

+ * The provided {@code mapper} function is guaranteed to be successfully invoked + * at most once per list index, even in a multi-threaded environment. Competing + * threads accessing an element already under computation will block until an element + * is computed or an exception is thrown by the computing thread. + *

+ * If invoking the provided {@code mapper} function throws an exception, it + * is rethrown to the initial caller and no value for the element is recorded. + *

+ * Any direct {@link List#subList(int, int) subList} or {@link List#reversed()} views + * of the returned list are also stable. + *

+ * The returned list and its {@link List#subList(int, int) subList} or + * {@link List#reversed()} views implement the {@link RandomAccess} interface. + *

+ * The returned list is unmodifiable and does not implement the + * {@linkplain Collection##optional-operation optional operations} in the + * {@linkplain List} interface. + *

+ * If the provided {@code mapper} recursively calls the returned list for the + * same index, an {@linkplain IllegalStateException} will be thrown. + * + * @param size the size of the returned list + * @param mapper to invoke whenever an element is first accessed + * (may return {@code null}) + * @param the type of elements in the returned list + * @throws IllegalArgumentException if the provided {@code size} is negative. + */ + static List list(int size, + IntFunction mapper) { + StableUtil.assertSizeNonNegative(size); + Objects.requireNonNull(mapper); + return SharedSecrets.getJavaUtilCollectionAccess().stableList(size, mapper); + } + + /** + * {@return a new stable map with the provided {@code keys}} + *

+ * The returned map is an {@linkplain Collection##unmodifiable unmodifiable} map whose + * keys are known at construction. The map's values are computed via the provided + * {@code mapper} when they are first accessed + * (e.g. via {@linkplain Map#get(Object) Map::get}). + *

+ * The provided {@code mapper} function is guaranteed to be successfully invoked + * at most once per key, even in a multi-threaded environment. Competing + * threads accessing a value already under computation will block until an element + * is computed or an exception is thrown by the computing thread. + *

+ * If invoking the provided {@code mapper} function throws an exception, it + * is rethrown to the initial caller and no value associated with the provided key + * is recorded. + *

+ * Any direct {@link Map#values()} or {@link Map#entrySet()} views + * of the returned map are also stable. + *

+ * The returned map is unmodifiable and does not implement the + * {@linkplain Collection##optional-operations optional operations} in the + * {@linkplain Map} interface. + *

+ * If the provided {@code mapper} recursively calls the returned map for + * the same key, an {@linkplain IllegalStateException} will be thrown. + * + * @param keys the (non-null) keys in the returned map + * @param mapper to invoke whenever an associated value is first accessed + * (may return {@code null}) + * @param the type of keys maintained by the returned map + * @param the type of mapped values in the returned map + * @throws NullPointerException if the provided set of {@code inputs} contains a + * {@code null} element. + */ + static Map map(Set keys, + Function mapper) { + Objects.requireNonNull(keys); + // Checking that the Set of keys does not contain a `null` value is made in the + // implementing class. + Objects.requireNonNull(mapper); + return SharedSecrets.getJavaUtilCollectionAccess().stableMap(keys, mapper); + } + +} diff --git a/src/java.base/share/classes/java/util/Collection.java b/src/java.base/share/classes/java/util/Collection.java index 0253dbc7e1a..43e8db55d7f 100644 --- a/src/java.base/share/classes/java/util/Collection.java +++ b/src/java.base/share/classes/java/util/Collection.java @@ -58,7 +58,7 @@ import java.util.stream.StreamSupport; * constructors) but all of the general-purpose {@code Collection} * implementations in the Java platform libraries comply. * - *

Certain methods are specified to be + *

Certain methods are specified to be * optional. If a collection implementation doesn't implement a * particular operation, it should define the corresponding method to throw * {@code UnsupportedOperationException}. Such methods are marked "optional diff --git a/src/java.base/share/classes/java/util/ImmutableCollections.java b/src/java.base/share/classes/java/util/ImmutableCollections.java index 205a6be6f89..9becf167176 100644 --- a/src/java.base/share/classes/java/util/ImmutableCollections.java +++ b/src/java.base/share/classes/java/util/ImmutableCollections.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -36,11 +36,19 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; 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.UnaryOperator; + import jdk.internal.access.JavaUtilCollectionAccess; import jdk.internal.access.SharedSecrets; +import jdk.internal.lang.stable.StableUtil; +import jdk.internal.lang.stable.StableValueImpl; import jdk.internal.misc.CDS; +import jdk.internal.util.ArraysSupport; +import jdk.internal.util.NullableKeyValueHolder; +import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.Stable; /** @@ -128,6 +136,12 @@ class ImmutableCollections { public List listFromTrustedArrayNullsAllowed(Object[] array) { return ImmutableCollections.listFromTrustedArrayNullsAllowed(array); } + public List stableList(int size, IntFunction mapper) { + return ImmutableCollections.stableList(size, mapper); + } + public Map stableMap(Set keys, Function mapper) { + return new StableMap<>(keys, mapper); + } }); } } @@ -250,6 +264,11 @@ class ImmutableCollections { } } + static List stableList(int size, IntFunction mapper) { + // A lazy list is not Serializable so, we cannot return `List.of()` if size == 0 + return new StableList<>(size, mapper); + } + // ---------- List Implementations ---------- @jdk.internal.ValueBased @@ -448,7 +467,7 @@ class ImmutableCollections { private final int size; private SubList(AbstractImmutableList root, int offset, int size) { - assert root instanceof List12 || root instanceof ListN; + assert root instanceof List12 || root instanceof ListN || root instanceof StableList; this.root = root; this.offset = offset; this.size = size; @@ -499,7 +518,8 @@ class ImmutableCollections { } private boolean allowNulls() { - return root instanceof ListN && ((ListN)root).allowNulls; + return root instanceof ListN listN && listN.allowNulls + || root instanceof StableList; } @Override @@ -551,6 +571,15 @@ class ImmutableCollections { } return array; } + + @Override + public String toString() { + if (root instanceof StableList stableList) { + return StableUtil.renderElements(root, "StableList", stableList.delegates, offset, size); + } else { + return super.toString(); + } + } } @jdk.internal.ValueBased @@ -768,6 +797,116 @@ class ImmutableCollections { } } + @jdk.internal.ValueBased + static final class StableList extends AbstractImmutableList { + + @Stable + private final IntFunction mapper; + @Stable + final StableValueImpl[] delegates; + + StableList(int size, IntFunction mapper) { + this.mapper = mapper; + this.delegates = StableUtil.array(size); + } + + @Override public boolean isEmpty() { return delegates.length == 0;} + @Override public int size() { return delegates.length; } + @Override public Object[] toArray() { return copyInto(new Object[size()]); } + + @ForceInline + @Override + public E get(int i) { + final StableValueImpl delegate; + try { + delegate = delegates[i]; + } catch (ArrayIndexOutOfBoundsException aioobe) { + throw new IndexOutOfBoundsException(i); + } + return delegate.orElseSet(new Supplier() { + @Override public E get() { return mapper.apply(i); }}); + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + final int size = delegates.length; + if (a.length < size) { + // Make a new array of a's runtime type, but my contents: + T[] n = (T[])Array.newInstance(a.getClass().getComponentType(), size); + return copyInto(n); + } + copyInto(a); + if (a.length > size) { + a[size] = null; // null-terminate + } + return a; + } + + @Override + public int indexOf(Object o) { + final int size = size(); + for (int i = 0; i < size; i++) { + if (Objects.equals(o, get(i))) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(Object o) { + for (int i = size() - 1; i >= 0; i--) { + if (Objects.equals(o, get(i))) { + return i; + } + } + return -1; + } + + @SuppressWarnings("unchecked") + private T[] copyInto(Object[] a) { + final int len = delegates.length; + for (int i = 0; i < len; i++) { + a[i] = get(i); + } + return (T[]) a; + } + + @Override + public List reversed() { + return new StableReverseOrderListView<>(this); + } + + @Override + public String toString() { + return StableUtil.renderElements(this, "StableList", delegates); + } + + private static final class StableReverseOrderListView extends ReverseOrderListView.Rand { + + private StableReverseOrderListView(List base) { + super(base, false); + } + + // This method does not evaluate the elements + @Override + public String toString() { + final StableValueImpl[] delegates = ((StableList)base).delegates; + final StableValueImpl[] reversed = ArraysSupport.reverse( + Arrays.copyOf(delegates, delegates.length)); + return StableUtil.renderElements(base, "Collection", reversed); + } + + @Override + public List reversed() { + return base; + } + + } + + } + // ---------- Set Implementations ---------- @jdk.internal.ValueBased @@ -1112,7 +1251,7 @@ class ImmutableCollections { // ---------- Map Implementations ---------- // Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap - abstract static class AbstractImmutableMap extends AbstractMap implements Serializable { + abstract static class AbstractImmutableMap extends AbstractMap { @Override public void clear() { throw uoe(); } @Override public V compute(K key, BiFunction rf) { throw uoe(); } @Override public V computeIfAbsent(K key, Function mf) { throw uoe(); } @@ -1143,7 +1282,7 @@ class ImmutableCollections { } // Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap - static final class Map1 extends AbstractImmutableMap { + static final class Map1 extends AbstractImmutableMap implements Serializable { @Stable private final K k0; @Stable @@ -1215,7 +1354,7 @@ class ImmutableCollections { * @param the value type */ // Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap - static final class MapN extends AbstractImmutableMap { + static final class MapN extends AbstractImmutableMap implements Serializable { @Stable final Object[] table; // pairs of key, value @@ -1405,6 +1544,130 @@ class ImmutableCollections { return new CollSer(CollSer.IMM_MAP, array); } } + + static final class StableMap + extends AbstractImmutableMap { + + @Stable + private final Function mapper; + @Stable + private final Map> delegate; + + StableMap(Set keys, Function mapper) { + this.mapper = mapper; + this.delegate = StableUtil.map(keys); + } + + @Override public boolean containsKey(Object o) { return delegate.containsKey(o); } + @Override public int size() { return delegate.size(); } + @Override public Set> entrySet() { return new StableMapEntrySet(); } + + @ForceInline + @Override + public V get(Object key) { + return getOrDefault(key, null); + } + + @ForceInline + @Override + public V getOrDefault(Object key, V defaultValue) { + final StableValueImpl stable = delegate.get(key); + if (stable == null) { + return defaultValue; + } + @SuppressWarnings("unchecked") + final K k = (K) key; + return stable.orElseSet(new Supplier() { + @Override public V get() { return mapper.apply(k); }}); + } + + @jdk.internal.ValueBased + final class StableMapEntrySet extends AbstractImmutableSet> { + + @Stable + private final Set>> delegateEntrySet; + + StableMapEntrySet() { + this.delegateEntrySet = delegate.entrySet(); + } + + @Override public Iterator> iterator() { return new LazyMapIterator(); } + @Override public int size() { return delegateEntrySet.size(); } + @Override public int hashCode() { return StableMap.this.hashCode(); } + + @Override + public String toString() { + return StableUtil.renderMappings(this, "StableSet", delegateEntrySet, false); + } + + @jdk.internal.ValueBased + final class LazyMapIterator implements Iterator> { + + @Stable + private final Iterator>> delegateIterator; + + LazyMapIterator() { + this.delegateIterator = delegateEntrySet.iterator(); + } + + @Override public boolean hasNext() { return delegateIterator.hasNext(); } + + @Override + public Entry next() { + final Map.Entry> inner = delegateIterator.next(); + final K k = inner.getKey(); + return new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier() { + @Override public V get() { return mapper.apply(k); }})); + } + + @Override + public void forEachRemaining(Consumer> action) { + final Consumer>> innerAction = + new Consumer<>() { + @Override + public void accept(Entry> inner) { + final K k = inner.getKey(); + action.accept(new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier() { + @Override public V get() { return mapper.apply(k); }}))); + } + }; + delegateIterator.forEachRemaining(innerAction); + } + } + } + + @Override + public Collection values() { + return new StableMapValues(); + } + + final class StableMapValues extends AbstractImmutableCollection { + @Override public Iterator iterator() { return new ValueIterator(); } + @Override public int size() { return StableMap.this.size(); } + @Override public boolean isEmpty() { return StableMap.this.isEmpty();} + @Override public boolean contains(Object v) { return StableMap.this.containsValue(v); } + + private static final IntFunction[]> GENERATOR = new IntFunction[]>() { + @Override + public StableValueImpl[] apply(int len) { + return new StableValueImpl[len]; + } + }; + + @Override + public String toString() { + final StableValueImpl[] values = delegate.values().toArray(GENERATOR); + return StableUtil.renderElements(StableMap.this, "StableMap", values); + } + } + + @Override + public String toString() { + return StableUtil.renderMappings(this, "StableMap", delegate.entrySet(), true); + } + + } + } // ---------- Serialization Proxy ---------- diff --git a/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java index f88d57521ac..cac8785b158 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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 @@ -26,8 +26,14 @@ package jdk.internal.access; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; public interface JavaUtilCollectionAccess { List listFromTrustedArray(Object[] array); List listFromTrustedArrayNullsAllowed(Object[] array); + List stableList(int size, IntFunction mapper); + Map stableMap(Set keys, Function mapper); } 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 deb786b42bd..2ed6dde794a 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -80,6 +80,8 @@ public @interface PreviewFeature { MODULE_IMPORTS, @JEP(number=478, title="Key Derivation Function API", status="Preview") KEY_DERIVATION, + @JEP(number = 502, title = "Stable Values", status = "Preview") + STABLE_VALUES, LANGUAGE_MODEL, /** * A key for testing. diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java new file mode 100644 index 00000000000..88be2cea1b6 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import jdk.internal.util.ImmutableBitSetPredicate; +import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Supplier; + +/** + * Optimized implementation of a stable Function with enums as keys. + * + * @implNote This implementation can be used early in the boot sequence as it does not + * rely on reflection, MethodHandles, Streams etc. + * + * @param firstOrdinal the lowest ordinal used + * @param delegates a delegate array of inputs to StableValue mappings + * @param original the original Function + * @param the type of the input to the function + * @param the type of the result of the function + */ +public record StableEnumFunction, R>(Class enumType, + int firstOrdinal, + IntPredicate member, + @Stable StableValueImpl[] delegates, + Function original) implements Function { + @ForceInline + @Override + public R apply(E value) { + if (!member.test(value.ordinal())) { // Implicit null-check of value + throw new IllegalArgumentException("Input not allowed: " + value); + } + final int index = value.ordinal() - firstOrdinal; + final StableValueImpl delegate; + // Since we did the member.test above, we know the index is in bounds + delegate = delegates[index]; + return delegate.orElseSet(new Supplier() { + @Override public R get() { return original.apply(value); }}); + + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public String toString() { + final E[] enumElements = enumType.getEnumConstants(); + final Collection>> entries = new ArrayList<>(enumElements.length); + int ordinal = firstOrdinal; + for (int i = 0; i < delegates.length; i++, ordinal++) { + if (member.test(ordinal)) { + entries.add(new AbstractMap.SimpleImmutableEntry<>(enumElements[ordinal], delegates[i])); + } + } + return StableUtil.renderMappings(this, "StableFunction", entries, true); + } + + @SuppressWarnings("unchecked") + public static , R> Function of(Set inputs, + Function original) { + // The input set is not empty + final Class enumType = (Class)inputs.iterator().next().getClass(); + final BitSet bitSet = new BitSet(enumType.getEnumConstants().length); + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + for (T t : inputs) { + final int ordinal = ((E) t).ordinal(); + min = Math.min(min, ordinal); + max = Math.max(max, ordinal); + bitSet.set(ordinal); + } + final int size = max - min + 1; + final IntPredicate member = ImmutableBitSetPredicate.of(bitSet); + return (Function) new StableEnumFunction(enumType, min, member, StableUtil.array(size), (Function) original); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java new file mode 100644 index 00000000000..1b10593e5e8 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import jdk.internal.vm.annotation.ForceInline; + +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +// Note: It would be possible to just use `LazyMap::get` with some additional logic +// instead of this class but explicitly providing a class like this provides better +// debug capability, exception handling, and may provide better performance. +/** + * Implementation of a stable Function. + * + * @implNote This implementation can be used early in the boot sequence as it does not + * rely on reflection, MethodHandles, Streams etc. + * + * @param values a delegate map of inputs to StableValue mappings + * @param original the original Function + * @param the type of the input to the function + * @param the type of the result of the function + */ +public record StableFunction(Map> values, + Function original) implements Function { + + @ForceInline + @Override + public R apply(T value) { + final StableValueImpl stable = values.get(value); + if (stable == null) { + throw new IllegalArgumentException("Input not allowed: " + value); + } + return stable.orElseSet(new Supplier() { + @Override public R get() { return original.apply(value); }}); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public String toString() { + return StableUtil.renderMappings(this, "StableFunction", values.entrySet(), true); + } + + public static StableFunction of(Set inputs, + Function original) { + return new StableFunction<>(StableUtil.map(inputs), original); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java new file mode 100644 index 00000000000..8bb046a762d --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; + +import java.util.function.IntFunction; +import java.util.function.Supplier; + +// Note: It would be possible to just use `LazyList::get` instead of this +// class but explicitly providing a class like this provides better +// debug capability, exception handling, and may provide better performance. +/** + * Implementation of a stable IntFunction. + *

+ * For performance reasons (~10%), we are not delegating to a StableList but are using + * the more primitive functions in StableValueUtil that are shared with StableList/StableValueImpl. + * + * @implNote This implementation can be used early in the boot sequence as it does not + * rely on reflection, MethodHandles, Streams etc. + * + * @param the return type + */ +public record StableIntFunction(@Stable StableValueImpl[] delegates, + IntFunction original) implements IntFunction { + + @ForceInline + @Override + public R apply(int index) { + final StableValueImpl delegate; + try { + delegate = delegates[index]; + } catch (ArrayIndexOutOfBoundsException ioob) { + throw new IllegalArgumentException("Input not allowed: " + index, ioob); + } + return delegate.orElseSet(new Supplier() { + @Override public R get() { return original.apply(index); }}); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public String toString() { + return StableUtil.renderElements(this, "StableIntFunction", delegates); + } + + public static StableIntFunction of(int size, IntFunction original) { + return new StableIntFunction<>(StableUtil.array(size), original); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java new file mode 100644 index 00000000000..bdb40648db6 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import jdk.internal.vm.annotation.ForceInline; + +import java.util.function.Supplier; + +/** + * Implementation of a stable supplier. + *

+ * @implNote This implementation can be used early in the boot sequence as it does not + * rely on reflection, MethodHandles, Streams etc. + * + * @param the return type + */ +public record StableSupplier(StableValueImpl delegate, + Supplier original) implements Supplier { + + @ForceInline + @Override + public T get() { + return delegate.orElseSet(original); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return obj == this; + } + + @Override + public String toString() { + final Object t = delegate.wrappedContentAcquire(); + return t == this ? "(this StableSupplier)" : StableValueImpl.renderWrapped(t); + } + + public static StableSupplier of(Supplier original) { + return new StableSupplier<>(StableValueImpl.of(), original); + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java new file mode 100644 index 00000000000..f6f33f9b1e8 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; + +public final class StableUtil { + + private StableUtil() {} + + public static String renderElements(Object self, + String selfName, + StableValueImpl[] delegates) { + return renderElements(self, selfName, delegates, 0, delegates.length); + } + + public static String renderElements(Object self, + String selfName, + StableValueImpl[] delegates, + int offset, + int length) { + final StringJoiner sj = new StringJoiner(", ", "[", "]"); + for (int i = 0; i < length; i++) { + final Object value = delegates[i + offset].wrappedContentAcquire(); + if (value == self) { + sj.add("(this " + selfName + ")"); + } else { + sj.add(StableValueImpl.renderWrapped(value)); + } + } + return sj.toString(); + } + + public static String renderMappings(Object self, + String selfName, + Iterable>> delegates, + boolean curly) { + final StringJoiner sj = new StringJoiner(", ", curly ? "{" : "[", curly ? "}" : "]"); + for (var e : delegates) { + final Object value = e.getValue().wrappedContentAcquire(); + final String valueString; + if (value == self) { + valueString = ("(this ") + selfName + ")"; + } else { + valueString = StableValueImpl.renderWrapped(value); + } + sj.add(e.getKey() + "=" + valueString); + } + return sj.toString(); + } + + public static StableValueImpl[] array(int size) { + assertSizeNonNegative(size); + @SuppressWarnings("unchecked") + final var stableValues = (StableValueImpl[]) new StableValueImpl[size]; + for (int i = 0; i < size; i++) { + stableValues[i] = StableValueImpl.of(); + } + return stableValues; + } + + public static Map> map(Set keys) { + Objects.requireNonNull(keys); + @SuppressWarnings("unchecked") + final var entries = (Map.Entry>[]) new Map.Entry[keys.size()]; + int i = 0; + for (K key : keys) { + entries[i++] = Map.entry(key, StableValueImpl.of()); + } + return Map.ofEntries(entries); + } + + public static void assertSizeNonNegative(int size) { + if (size < 0) { + throw new IllegalArgumentException("size can not be negative: " + size); + } + } + +} diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java new file mode 100644 index 00000000000..88c80eb6b39 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2025, 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 jdk.internal.lang.stable; + +import jdk.internal.misc.Unsafe; +import jdk.internal.vm.annotation.DontInline; +import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * The implementation of StableValue. + * + * @implNote This implementation can be used early in the boot sequence as it does not + * rely on reflection, MethodHandles, Streams etc. + * + * @param type of the contents + */ +public final class StableValueImpl implements StableValue { + + static final String UNSET_LABEL = ".unset"; + + // Unsafe allows StableValue to be used early in the boot sequence + static final Unsafe UNSAFE = Unsafe.getUnsafe(); + + // Unsafe offsets for direct field access + + private static final long CONTENT_OFFSET = + UNSAFE.objectFieldOffset(StableValueImpl.class, "contents"); + // Used to indicate a holder value is `null` (see field `value` below) + // A wrapper method `nullSentinel()` is used for generic type conversion. + private static final Object NULL_SENTINEL = new Object(); + + // Generally, fields annotated with `@Stable` are accessed by the JVM using special + // memory semantics rules (see `parse.hpp` and `parse(1|2|3).cpp`). + // + // This field is used directly and reflectively via Unsafe using explicit memory semantics. + // + // | Value | Meaning | + // | -------------- | ------------ | + // | null | Unset | + // | NULL_SENTINEL | Set(null) | + // | other | Set(other) | + // + @Stable + private Object contents; + + // Only allow creation via the factory `StableValueImpl::newInstance` + private StableValueImpl() {} + + @ForceInline + @Override + public boolean trySet(T contents) { + if (wrappedContentAcquire() != null) { + return false; + } + // Prevent reentry via an orElseSet(supplier) + preventReentry(); + // Mutual exclusion is required here as `orElseSet` might also + // attempt to modify the `wrappedValue` + synchronized (this) { + return wrapAndSet(contents); + } + } + + @ForceInline + @Override + public void setOrThrow(T contents) { + if (!trySet(contents)) { + // Neither the set contents nor the provided contents is revealed in the + // exception message as it might be sensitive. + throw new IllegalStateException("The contents is already set"); + } + } + + @ForceInline + @Override + public T orElseThrow() { + final Object t = wrappedContentAcquire(); + if (t == null) { + throw new NoSuchElementException("No contents set"); + } + return unwrap(t); + } + + @ForceInline + @Override + public T orElse(T other) { + final Object t = wrappedContentAcquire(); + return (t == null) ? other : unwrap(t); + } + + @ForceInline + @Override + public boolean isSet() { + return wrappedContentAcquire() != null; + } + + @ForceInline + @Override + public T orElseSet(Supplier supplier) { + Objects.requireNonNull(supplier); + final Object t = wrappedContentAcquire(); + return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t); + } + + @DontInline + private T orElseSetSlowPath(Supplier supplier) { + preventReentry(); + synchronized (this) { + final Object t = contents; // Plain semantics suffice here + if (t == null) { + final T newValue = supplier.get(); + // The mutex is not reentrant so we know newValue should be returned + wrapAndSet(newValue); + return newValue; + } + return unwrap(t); + } + } + + // The methods equals() and hashCode() should be based on identity (defaults from Object) + + @Override + public String toString() { + final Object t = wrappedContentAcquire(); + return t == this + ? "(this StableValue)" + : renderWrapped(t); + } + + // Internal methods shared with other internal classes + + @ForceInline + public Object wrappedContentAcquire() { + return UNSAFE.getReferenceAcquire(this, CONTENT_OFFSET); + } + + static String renderWrapped(Object t) { + return (t == null) ? UNSET_LABEL : Objects.toString(unwrap(t)); + } + + // Private methods + + // This method is not annotated with @ForceInline as it is always called + // in a slow path. + private void preventReentry() { + if (Thread.holdsLock(this)) { + throw new IllegalStateException("Recursive initialization of a stable value is illegal"); + } + } + + /** + * Wraps the provided {@code newValue} and tries to set the contents. + *

+ * This method ensures the {@link Stable} field is written to at most once. + * + * @param newValue to wrap and set + * @return if the contents was set + */ + @ForceInline + private boolean wrapAndSet(Object newValue) { + assert Thread.holdsLock(this); + // We know we hold the monitor here so plain semantic is enough + if (contents == null) { + UNSAFE.putReferenceRelease(this, CONTENT_OFFSET, wrap(newValue)); + return true; + } + return false; + } + + + // Wraps `null` values into a sentinel value + @ForceInline + private static Object wrap(Object t) { + return (t == null) ? NULL_SENTINEL : t; + } + + // Unwraps null sentinel values into `null` + @SuppressWarnings("unchecked") + @ForceInline + private static T unwrap(Object t) { + return t != NULL_SENTINEL ? (T) t : null; + } + + // Factory + + public static StableValueImpl of() { + return new StableValueImpl<>(); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableFunctionTest.java b/test/jdk/java/lang/StableValue/StableFunctionTest.java new file mode 100644 index 00000000000..4476c046957 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableFunctionTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableFunction methods + * @enablePreview + * @run junit StableFunctionTest + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableFunctionTest { + + enum Value { + // Zero is here so that we have enums with ordinals before the first one + // actually used in input sets (i.e. ZERO is not in the input set) + ZERO(0), + ILLEGAL_BEFORE(-1), + // Valid values + THIRTEEN(13), + ILLEGAL_BETWEEN(-2), + FORTY_TWO(42), + // Illegal values (not in the input set) + ILLEGAL_AFTER(-3); + + final int intValue; + + Value(int intValue) { + this.intValue = intValue; + } + + int asInt() { + return intValue; + } + + } + + private static final Function MAPPER = Value::asInt; + + @ParameterizedTest + @MethodSource("allSets") + void factoryInvariants(Set inputs) { + assertThrows(NullPointerException.class, () -> StableValue.function(null, MAPPER)); + assertThrows(NullPointerException.class, () -> StableValue.function(inputs, null)); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void basic(Set inputs) { + basic(inputs, MAPPER); + toStringTest(inputs, MAPPER); + basic(inputs, _ -> null); + toStringTest(inputs, _ -> null); + } + + void basic(Set inputs, Function mapper) { + StableTestUtil.CountingFunction cif = new StableTestUtil.CountingFunction<>(mapper); + var cached = StableValue.function(inputs, cif); + assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO)); + assertEquals(1, cif.cnt()); + assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO)); + assertEquals(1, cif.cnt()); + var x0 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BEFORE)); + assertEquals("Input not allowed: ILLEGAL_BEFORE", x0.getMessage()); + var x1 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BETWEEN)); + assertEquals("Input not allowed: ILLEGAL_BETWEEN", x1.getMessage()); + var x2 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_AFTER)); + assertEquals("Input not allowed: ILLEGAL_AFTER", x2.getMessage()); + } + + void toStringTest(Set inputs, Function mapper) { + var cached = StableValue.function(inputs, mapper); + cached.apply(Value.FORTY_TWO); + var toString = cached.toString(); + assertTrue(toString.startsWith("{")); + // Key order is unspecified + assertTrue(toString.contains(Value.THIRTEEN + "=.unset")); + assertTrue(toString.contains(Value.FORTY_TWO + "=" + mapper.apply(Value.FORTY_TWO))); + assertTrue(toString.endsWith("}")); + // One between the values + assertEquals(1L, toString.chars().filter(ch -> ch == ',').count()); + } + + @ParameterizedTest + @MethodSource("emptySets") + void empty(Set inputs) { + Function f0 = StableValue.function(inputs, Value::asInt); + Function f1 = StableValue.function(inputs, Value::asInt); + assertEquals("{}", f0.toString()); + assertThrows(NullPointerException.class, () -> f0.apply(null)); + assertNotEquals(f0, f1); + assertNotEquals(null, f0); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void exception(Set inputs) { + StableTestUtil.CountingFunction cif = new StableTestUtil.CountingFunction<>(_ -> { + throw new UnsupportedOperationException(); + }); + var cached = StableValue.function(inputs, cif); + assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO)); + assertEquals(1, cif.cnt()); + assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO)); + assertEquals(2, cif.cnt()); + var toString = cached.toString(); + assertTrue(toString.startsWith("{")); + // Key order is unspecified + assertTrue(toString.contains(Value.THIRTEEN + "=.unset")); + assertTrue(toString.contains(Value.FORTY_TWO + "=.unset")); + assertTrue(toString.endsWith("}")); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void circular(Set inputs) { + final AtomicReference> ref = new AtomicReference<>(); + Function> cached = StableValue.function(inputs, _ -> ref.get()); + ref.set(cached); + cached.apply(Value.FORTY_TWO); + var toString = cached.toString(); + assertTrue(toString.contains("FORTY_TWO=(this StableFunction)"), toString); + assertDoesNotThrow(cached::hashCode); + assertDoesNotThrow((() -> cached.equals(cached))); + } + + @ParameterizedTest + @MethodSource("allSets") + void equality(Set inputs) { + Function mapper = Value::asInt; + Function f0 = StableValue.function(inputs, mapper); + Function f1 = StableValue.function(inputs, mapper); + // No function is equal to another function + assertNotEquals(f0, f1); + } + + @ParameterizedTest + @MethodSource("allSets") + void hashCodeStable(Set inputs) { + Function f0 = StableValue.function(inputs, Value::asInt); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + if (!inputs.isEmpty()) { + f0.apply(Value.FORTY_TWO); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + } + } + + @Test + void nullKeys() { + Set inputs = new HashSet<>(); + inputs.add(Value.FORTY_TWO); + inputs.add(null); + assertThrows(NullPointerException.class, () -> StableValue.function(inputs, MAPPER)); + } + + @Test + void usesOptimizedVersion() { + Function enumFunction = StableValue.function(EnumSet.of(Value.FORTY_TWO), Value::asInt); + assertEquals("jdk.internal.lang.stable.StableEnumFunction", enumFunction.getClass().getName()); + Function emptyFunction = StableValue.function(Set.of(), Value::asInt); + assertEquals("jdk.internal.lang.stable.StableFunction", emptyFunction.getClass().getName()); + } + + private static Stream> nonEmptySets() { + return Stream.of( + Set.of(Value.FORTY_TWO, Value.THIRTEEN), + linkedHashSet(Value.THIRTEEN, Value.FORTY_TWO), + treeSet(Value.FORTY_TWO, Value.THIRTEEN), + EnumSet.of(Value.FORTY_TWO, Value.THIRTEEN) + ); + } + + private static Stream> emptySets() { + return Stream.of( + Set.of(), + linkedHashSet(), + treeSet(), + EnumSet.noneOf(Value.class) + ); + } + + private static Stream> allSets() { + return Stream.concat( + nonEmptySets(), + emptySets() + ); + } + + static Set treeSet(Value... values) { + return populate(new TreeSet<>(Comparator.comparingInt(Value::asInt).reversed()),values); + } + + static Set linkedHashSet(Value... values) { + return populate(new LinkedHashSet<>(), values); + } + + static Set populate(Set set, Value... values) { + set.addAll(Arrays.asList(values)); + return set; + } + +} diff --git a/test/jdk/java/lang/StableValue/StableIntFunctionTest.java b/test/jdk/java/lang/StableValue/StableIntFunctionTest.java new file mode 100644 index 00000000000..7397a688ee6 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableIntFunctionTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableIntFunction methods + * @enablePreview + * @run junit StableIntFunctionTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntFunction; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableIntFunctionTest { + + private static final int SIZE = 2; + private static final IntFunction MAPPER = i -> i; + + @Test + void factoryInvariants() { + assertThrows(IllegalArgumentException.class, () -> StableValue.intFunction(-1, MAPPER)); + assertThrows(NullPointerException.class, () -> StableValue.intFunction(SIZE, null)); + } + + @Test + void basic() { + basic(MAPPER); + basic(i -> null); + } + + void basic(IntFunction mapper) { + StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(mapper); + var cached = StableValue.intFunction(SIZE, cif); + assertEquals("[.unset, .unset]", cached.toString()); + assertEquals(mapper.apply(1), cached.apply(1)); + assertEquals(1, cif.cnt()); + assertEquals(mapper.apply(1), cached.apply(1)); + assertEquals(1, cif.cnt()); + assertEquals("[.unset, " + mapper.apply(1) + "]", cached.toString()); + assertThrows(IllegalArgumentException.class, () -> cached.apply(SIZE)); + assertThrows(IllegalArgumentException.class, () -> cached.apply(-1)); + assertThrows(IllegalArgumentException.class, () -> cached.apply(1_000_000)); + } + + @Test + void exception() { + StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(_ -> { + throw new UnsupportedOperationException(); + }); + var cached = StableValue.intFunction(SIZE, cif); + assertThrows(UnsupportedOperationException.class, () -> cached.apply(1)); + assertEquals(1, cif.cnt()); + assertThrows(UnsupportedOperationException.class, () -> cached.apply(1)); + assertEquals(2, cif.cnt()); + assertEquals("[.unset, .unset]", cached.toString()); + } + + @Test + void circular() { + final AtomicReference> ref = new AtomicReference<>(); + IntFunction> cached = StableValue.intFunction(SIZE, _ -> ref.get()); + ref.set(cached); + cached.apply(0); + String toString = cached.toString(); + assertEquals("[(this StableIntFunction), .unset]", toString); + assertDoesNotThrow(cached::hashCode); + assertDoesNotThrow((() -> cached.equals(cached))); + } + + @Test + void equality() { + IntFunction f0 = StableValue.intFunction(8, MAPPER); + IntFunction f1 = StableValue.intFunction(8, MAPPER); + // No function is equal to another function + assertNotEquals(f0, f1); + } + + @Test + void hashCodeStable() { + IntFunction f0 = StableValue.intFunction(8, MAPPER); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + f0.apply(4); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableListTest.java b/test/jdk/java/lang/StableValue/StableListTest.java new file mode 100644 index 00000000000..f0ae081c76c --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableListTest.java @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableList methods + * @modules java.base/jdk.internal.lang.stable + * @enablePreview + * @run junit StableListTest + */ + +import jdk.internal.lang.stable.StableUtil; +import jdk.internal.lang.stable.StableValueImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableListTest { + + private static final int ZERO = 0; + private static final int INDEX = 7; + private static final int SIZE = 31; + private static final IntFunction IDENTITY = i -> i; + + @Test + void factoryInvariants() { + assertThrows(NullPointerException.class, () -> StableValue.list(SIZE, null)); + assertThrows(IllegalArgumentException.class, () -> StableValue.list(-1, IDENTITY)); + } + + @Test + void isEmpty() { + assertFalse(newList().isEmpty()); + assertTrue(newEmptyList().isEmpty()); + } + + @Test + void size() { + assertEquals(SIZE, newList().size()); + assertEquals(ZERO, newEmptyList().size()); + } + + @Test + void get() { + StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(IDENTITY); + var lazy = StableValue.list(SIZE, cif); + for (int i = 0; i < SIZE; i++) { + assertEquals(i, lazy.get(i)); + assertEquals(i + 1, cif.cnt()); + assertEquals(i, lazy.get(i)); + assertEquals(i + 1, cif.cnt()); + } + } + + @Test + void getException() { + StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(_ -> { + throw new UnsupportedOperationException(); + }); + var lazy = StableValue.list(SIZE, cif); + assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX)); + assertEquals(1, cif.cnt()); + assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX)); + assertEquals(2, cif.cnt()); + } + + @Test + void toArray() { + assertArrayEquals(new Object[ZERO], newEmptyList().toArray()); + assertArrayEquals(newRegularList().toArray(), newList().toArray()); + } + + @Test + void toArrayWithArrayLarger() { + Integer[] actual = new Integer[SIZE]; + for (int i = 0; i < SIZE; i++) { + actual[INDEX] = 100 + i; + } + var list = StableValue.list(INDEX, IDENTITY); + assertSame(actual, list.toArray(actual)); + Integer[] expected = IntStream.range(0, SIZE) + .mapToObj(i -> i < INDEX ? i : null) + .toArray(Integer[]::new); + assertArrayEquals(expected, actual); + } + + @Test + void toArrayWithArraySmaller() { + Integer[] arr = new Integer[INDEX]; + Integer[] actual = newList().toArray(arr); + assertNotSame(arr, actual); + Integer[] expected = newRegularList().toArray(new Integer[0]); + assertArrayEquals(expected, actual); + } + + @Test + void toArrayWithGenerator() { + Integer[] expected = newRegularList().toArray(Integer[]::new); + Integer[] actual = newList().toArray(Integer[]::new); + assertArrayEquals(expected, actual); + } + + @Test + void firstIndex() { + var lazy = newList(); + for (int i = INDEX; i < SIZE; i++) { + assertEquals(i, lazy.indexOf(i)); + } + assertEquals(-1, lazy.indexOf(SIZE + 1)); + } + + @Test + void lastIndex() { + var lazy = newList(); + for (int i = INDEX; i < SIZE; i++) { + assertEquals(i, lazy.lastIndexOf(i)); + } + assertEquals(-1, lazy.lastIndexOf(SIZE + 1)); + } + + @Test + void toStringTest() { + assertEquals("[]", newEmptyList().toString()); + var list = StableValue.list(2, IDENTITY); + assertEquals("[.unset, .unset]", list.toString()); + list.get(0); + assertEquals("[0, .unset]", list.toString()); + list.get(1); + assertEquals("[0, 1]", list.toString()); + } + + @Test + void hashCodeTest() { + assertEquals(List.of().hashCode(), newEmptyList().hashCode()); + assertEquals(newRegularList().hashCode(), newList().hashCode()); + } + + @Test + void equalsTest() { + assertTrue(newEmptyList().equals(List.of())); + assertTrue(List.of().equals(newEmptyList())); + assertTrue(newList().equals(newRegularList())); + assertTrue(newRegularList().equals(newList())); + assertFalse(newList().equals("A")); + } + + @Test + void equalsPartialEvaluationTest() { + var list = StableValue.list(2, IDENTITY); + assertFalse(list.equals(List.of(0))); + assertEquals("[0, .unset]", list.toString()); + assertTrue(list.equals(List.of(0, 1))); + assertEquals("[0, 1]", list.toString()); + } + + @Test + void iteratorTotal() { + var iterator = newList().iterator(); + for (int i = 0; i < SIZE; i++) { + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasNext()); + assertEquals(i, iterator.next()); + } + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + AtomicInteger cnt = new AtomicInteger(); + iterator.forEachRemaining(_ -> cnt.incrementAndGet()); + assertEquals(0, cnt.get()); + } + + @Test + void iteratorPartial() { + var iterator = newList().iterator(); + for (int i = 0; i < INDEX; i++) { + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasNext()); + assertEquals(i, iterator.next()); + } + assertTrue(iterator.hasNext()); + AtomicInteger cnt = new AtomicInteger(); + iterator.forEachRemaining(_ -> cnt.incrementAndGet()); + assertEquals(SIZE - INDEX, cnt.get()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void subList() { + var lazy = newList(); + var lazySubList = lazy.subList(1, SIZE); + assertInstanceOf(RandomAccess.class, lazySubList); + var regularList = newRegularList(); + var regularSubList = regularList.subList(1, SIZE); + assertEquals(regularSubList, lazySubList); + } + + @Test + void subList2() { + var lazy = newList(); + var lazySubList = lazy.subList(1, SIZE); + lazySubList.get(0); + var eq = newList(); + eq.get(1); + assertEquals(eq.toString(), lazy.toString()); + } + + @Test + void subListToString() { + subListToString0(newList()); + subListToString0(newList().subList(1, SIZE)); + subListToString0(newList().subList(1, SIZE).subList(0, SIZE - 2)); + } + + void subListToString0(List subList) { + assertEquals(asString(".unset", subList), subList.toString()); + + var first = subList.getFirst(); + assertEquals(asString(first.toString(), subList), subList.toString()); + } + + @Test + void reversed() { + var reversed = newList().reversed(); + assertInstanceOf(RandomAccess.class, reversed); + assertEquals(SIZE - 1, reversed.getFirst()); + assertEquals(0, reversed.getLast()); + + var reversed2 = reversed.reversed(); + assertInstanceOf(RandomAccess.class, reversed2); + assertEquals(0, reversed2.getFirst()); + assertEquals(SIZE - 1, reversed2.getLast()); + // Make sure we get back a non-reversed implementation + assertEquals("java.util.ImmutableCollections$StableList", reversed2.getClass().getName()); + } + + @Test + void reversedToString() { + var reversed = newList().reversed(); + subListToString0(reversed); + } + + @Test + void subListReversedToString() { + var list = newList().subList(1, SIZE - 1).reversed(); + // This combination is not lazy. There has to be a limit somewhere. + var regularList = newRegularList().subList(1, SIZE - 1).reversed(); + assertEquals(regularList.toString(), list.toString()); + } + + @Test + void recursiveCall() { + AtomicReference> ref = new AtomicReference<>(); + var lazy = StableValue.list(SIZE, i -> ref.get().apply(i)); + ref.set(lazy::get); + var x = assertThrows(IllegalStateException.class, () -> lazy.get(INDEX)); + assertEquals("Recursive initialization of a stable value is illegal", x.getMessage()); + } + + // Immutability + + @ParameterizedTest + @MethodSource("unsupportedOperations") + void unsupported(Operation operation) { + assertThrowsForOperation(UnsupportedOperationException.class, operation); + } + + // Method parameter invariant checking + + @ParameterizedTest + @MethodSource("nullAverseOperations") + void nullAverse(Operation operation) { + assertThrowsForOperation(NullPointerException.class, operation); + } + + @ParameterizedTest + @MethodSource("outOfBoundsOperations") + void outOfBounds(Operation operation) { + assertThrowsForOperation(IndexOutOfBoundsException.class, operation); + } + + static void assertThrowsForOperation(Class expectedType, Operation operation) { + var lazy = newList(); + assertThrows(expectedType, () -> operation.accept(lazy)); + var sub = lazy.subList(1, SIZE / 2); + assertThrows(expectedType, () -> operation.accept(sub)); + var subSub = sub.subList(1, sub.size() / 2); + assertThrows(expectedType, () -> operation.accept(subSub)); + } + + // Implementing interfaces + + @Test + void serializable() { + serializable(newList()); + serializable(newEmptyList()); + } + + void serializable(List list) { + assertFalse(list instanceof Serializable); + if (list.size()>INDEX) { + assertFalse(newList().subList(1, INDEX) instanceof Serializable); + } + assertFalse(list.iterator() instanceof Serializable); + assertFalse(list.reversed() instanceof Serializable); + assertFalse(list.spliterator() instanceof Serializable); + } + + @Test + void randomAccess() { + assertInstanceOf(RandomAccess.class, newList()); + assertInstanceOf(RandomAccess.class, newEmptyList()); + assertInstanceOf(RandomAccess.class, newList().subList(1, INDEX)); + } + + @Test + void distinct() { + StableValueImpl[] array = StableUtil.array(SIZE); + assertEquals(SIZE, array.length); + // Check, every StableValue is distinct + Map, Boolean> idMap = new IdentityHashMap<>(); + for (var e: array) { + idMap.put(e, true); + } + assertEquals(SIZE, idMap.size()); + } + + // Support constructs + + record Operation(String name, + Consumer> consumer) implements Consumer> { + @Override public void accept(List list) { consumer.accept(list); } + @Override public String toString() { return name; } + } + + static Stream nullAverseOperations() { + return Stream.of( + new Operation("forEach", l -> l.forEach(null)), + new Operation("containsAll", l -> l.containsAll(null)), + new Operation("toArray", l -> l.toArray((Integer[]) null)), + new Operation("toArray", l -> l.toArray((IntFunction) null)) + ); + } + + static Stream outOfBoundsOperations() { + return Stream.of( + new Operation("get(-1)", l -> l.get(-1)), + new Operation("get(size)", l -> l.get(l.size())), + new Operation("sublist(-1,)", l -> l.subList(-1, INDEX)), + new Operation("sublist(,size)", l -> l.subList(0, l.size() + 1)), + new Operation("listIter(-1)", l -> l.listIterator(-1)), + new Operation("listIter(size)", l -> l.listIterator(l.size() + 1)) + ); + } + + static Stream unsupportedOperations() { + final Set SET = Set.of(0, 1); + return Stream.of( + new Operation("add(0)", l -> l.add(0)), + new Operation("add(0, 1)", l -> l.add(0, 1)), + new Operation("addAll(col)", l -> l.addAll(SET)), + new Operation("addAll(1, coll)", l -> l.addAll(1, SET)), + new Operation("addFirst(0)", l -> l.addFirst(0)), + new Operation("addLast(0)", l -> l.addLast(0)), + new Operation("clear", List::clear), + new Operation("remove(Obj)", l -> l.remove((Object)1)), + new Operation("remove(1)", l -> l.remove(1)), + new Operation("removeAll", l -> l.removeAll(SET)), + new Operation("removeFirst", List::removeFirst), + new Operation("removeLast", List::removeLast), + new Operation("removeIf", l -> l.removeIf(i -> i % 2 == 0)), + new Operation("replaceAll", l -> l.replaceAll(i -> i + 1)), + new Operation("sort", l -> l.sort(Comparator.naturalOrder())), + new Operation("iterator().remove", l -> l.iterator().remove()), + new Operation("listIter().remove", l -> l.listIterator().remove()), + new Operation("listIter().add", l -> l.listIterator().add(1)), + new Operation("listIter().set", l -> l.listIterator().set(1)) + ); + } + + static List newList() { + return StableValue.list(SIZE, IDENTITY); + } + + static List newEmptyList() { + return StableValue.list(ZERO, IDENTITY); + } + + static List newRegularList() { + return IntStream.range(0, SIZE).boxed().toList(); + } + + static String asString(String first, List list) { + return "[" + first + ", " + Stream.generate(() -> ".unset") + .limit(list.size() - 1) + .collect(Collectors.joining(", ")) + "]"; + } + +} diff --git a/test/jdk/java/lang/StableValue/StableMapTest.java b/test/jdk/java/lang/StableValue/StableMapTest.java new file mode 100644 index 00000000000..3133acc2f29 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableMapTest.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableMap methods + * @modules java.base/jdk.internal.lang.stable + * @enablePreview + * @run junit StableMapTest + */ + +import jdk.internal.lang.stable.StableUtil; +import jdk.internal.lang.stable.StableValueImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableMapTest { + + private static final int NOT_PRESENT = 147; + private static final int KEY = 7; + private static final Set KEYS = Set.of(0, KEY, 13); + private static final Set EMPTY = Set.of(); + private static final Function IDENTITY = Function.identity(); + + @Test + void factoryInvariants() { + assertThrows(NullPointerException.class, () -> StableValue.map(KEYS, null)); + assertThrows(NullPointerException.class, () -> StableValue.map(null, IDENTITY)); + } + + @Test + void isEmpty() { + assertFalse(newMap().isEmpty()); + assertTrue(newEmptyMap().isEmpty()); + } + + @Test + void size() { + assertEquals(KEYS.size(), newMap().size()); + assertEquals(EMPTY.size(), newEmptyMap().size()); + } + + @Test + void get() { + StableTestUtil.CountingFunction cf = new StableTestUtil.CountingFunction<>(IDENTITY); + var lazy = StableValue.map(KEYS, cf); + int cnt = 1; + for (int i : KEYS) { + assertEquals(i, lazy.get(i)); + assertEquals(cnt, cf.cnt()); + assertEquals(i, lazy.get(i)); + assertEquals(cnt++, cf.cnt()); + } + assertNull(lazy.get(NOT_PRESENT)); + } + + @Test + void getException() { + StableTestUtil.CountingFunction cf = new StableTestUtil.CountingFunction<>(_ -> { + throw new UnsupportedOperationException(); + }); + var lazy = StableValue.map(KEYS, cf); + assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY)); + assertEquals(1, cf.cnt()); + assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY)); + assertEquals(2, cf.cnt()); + } + + @Test + void containsKey() { + var lazy = newMap(); + for (int i : KEYS) { + assertTrue(lazy.containsKey(i)); + } + assertFalse(lazy.containsKey(NOT_PRESENT)); + } + + @Test + void containsValue() { + var lazy = newMap(); + for (int i : KEYS) { + assertTrue(lazy.containsValue(i)); + } + assertFalse(lazy.containsValue(NOT_PRESENT)); + } + + @Test + void forEach() { + var lazy = newMap(); + Set> expected = KEYS.stream() + .map(i -> new AbstractMap.SimpleImmutableEntry<>(i , i)) + .collect(Collectors.toSet()); + Set> actual = new HashSet<>(); + lazy.forEach((k, v) -> actual.add(new AbstractMap.SimpleImmutableEntry<>(k , v))); + assertEquals(expected, actual); + } + + @Test + void toStringTest() { + assertEquals("{}", newEmptyMap().toString()); + var map = StableValue.map(Set.of(KEY), IDENTITY); + assertEquals("{" + KEY + "=.unset}", map.toString()); + map.get(KEY); + assertEquals("{" + KEY + "=" + KEY + "}", map.toString()); + String actual = newMap().toString(); + assertTrue(actual.startsWith("{")); + for (int key : KEYS) { + assertTrue(actual.contains(key + "=.unset")); + } + assertTrue(actual.endsWith("}")); + } + + @Test + void hashCodeTest() { + assertEquals(Map.of().hashCode(), newEmptyMap().hashCode()); + assertEquals(newRegularMap().hashCode(), newMap().hashCode()); + } + + @Test + void equalsTest() { + assertTrue(newEmptyMap().equals(Map.of())); + assertTrue(Map.of().equals(newEmptyMap())); + assertTrue(newMap().equals(newRegularMap())); + assertTrue(newRegularMap().equals(newMap())); + assertFalse(newMap().equals("A")); + } + + @Test + void entrySet() { + var regular = newRegularMap().entrySet(); + var actual = newMap().entrySet(); + assertTrue(regular.equals(actual)); + assertTrue(actual.equals(regular)); + assertTrue(regular.equals(actual)); + } + + @Test + void entrySetToString() { + var map = newMap(); + var entrySet = map.entrySet(); + var toString = entrySet.toString(); + for (var key : KEYS) { + assertTrue(toString.contains(key + "=.unset")); + } + assertTrue(toString.startsWith("[")); + assertTrue(toString.endsWith("]")); + + map.get(KEY); + for (var key : KEYS) { + if (key.equals(KEY)) { + continue; + } + assertTrue(entrySet.toString().contains(key + "=.unset")); + } + assertTrue(entrySet.toString().contains(KEY + "=" + KEY)); + } + + @Test + void values() { + var map = newMap(); + var values = map.values(); + // Look at one of the elements + var val = values.stream().iterator().next(); + var toString = map.toString(); + for (var key : KEYS) { + if (key.equals(val)) { + assertTrue(toString.contains(key + "=" + key)); + } else { + assertTrue(toString.contains(key + "=.unset")); + } + } + + // Mod ops + assertThrows(UnsupportedOperationException.class, () -> values.remove(KEY)); + assertThrows(UnsupportedOperationException.class, () -> values.add(KEY)); + assertThrows(UnsupportedOperationException.class, values::clear); + assertThrows(UnsupportedOperationException.class, () -> values.addAll(Set.of(1))); + assertThrows(UnsupportedOperationException.class, () -> values.removeIf(i -> true)); + assertThrows(UnsupportedOperationException.class, () -> values.retainAll(Set.of(KEY))); + } + + @Test + void valuesToString() { + var map = newMap(); + var values = map.values(); + assertEquals("[.unset, .unset, .unset]", values.toString()); + map.get(KEY); + var afterGet = values.toString(); + assertTrue(afterGet.contains(Integer.toString(KEY)), afterGet); + } + + @Test + void iteratorNext() { + Set encountered = new HashSet<>(); + var iterator = newMap().entrySet().iterator(); + while (iterator.hasNext()) { + var entry = iterator.next(); + assertEquals(entry.getKey(), entry.getValue()); + encountered.add(entry.getValue()); + } + assertEquals(KEYS, encountered); + } + + @Test + void iteratorForEachRemaining() { + Set encountered = new HashSet<>(); + var iterator = newMap().entrySet().iterator(); + var entry = iterator.next(); + assertEquals(entry.getKey(), entry.getValue()); + encountered.add(entry.getValue()); + iterator.forEachRemaining(e -> { + assertEquals(e.getKey(), e.getValue()); + encountered.add(e.getValue()); + }); + assertEquals(KEYS, encountered); + } + + // Immutability + @ParameterizedTest + @MethodSource("unsupportedOperations") + void unsupported(Operation operation) { + assertThrowsForOperation(UnsupportedOperationException.class, operation); + } + + // Method parameter invariant checking + + @ParameterizedTest + @MethodSource("nullAverseOperations") + void nullAverse(Operation operation) { + assertThrowsForOperation(NullPointerException.class, operation); + } + + static void assertThrowsForOperation(Class expectedType, Operation operation) { + var lazy = newMap(); + assertThrows(expectedType, () -> operation.accept(lazy)); + } + + // Implementing interfaces + + @Test + void serializable() { + serializable(newMap()); + serializable(newEmptyMap()); + } + + void serializable(Map map) { + assertFalse(map instanceof Serializable); + assertFalse(map.entrySet() instanceof Serializable); + assertFalse(map.keySet() instanceof Serializable); + assertFalse(map.values() instanceof Serializable); + } + + @Test + void distinct() { + Map> map = StableUtil.map(Set.of(1, 2, 3)); + assertEquals(3, map.size()); + // Check, every StableValue is distinct + Map, Boolean> idMap = new IdentityHashMap<>(); + map.forEach((k, v) -> idMap.put(v, true)); + assertEquals(3, idMap.size()); + } + + @Test + void nullResult() { + var map = StableValue.map(Set.of(0), _ -> null); + assertNull(map.getOrDefault(0, 1));; + assertTrue(map.containsKey(0)); + assertNull(map.get(0)); + } + + @Test + void nullKeys() { + Set inputs = new HashSet<>(); + inputs.add(0); + inputs.add(null); + assertThrows(NullPointerException.class, () -> StableValue.map(inputs, IDENTITY)); + } + + // Support constructs + + record Operation(String name, + Consumer> consumer) implements Consumer> { + @java.lang.Override + public void accept(Map map) { consumer.accept(map); } + @java.lang.Override + public String toString() { return name; } + } + + static Stream nullAverseOperations() { + return Stream.of( + new Operation("forEach", m -> m.forEach(null)) + ); + } + + static Stream unsupportedOperations() { + return Stream.of( + new Operation("clear", Map::clear), + new Operation("compute", m -> m.compute(KEY, (_, _) -> 1)), + new Operation("computeIfAbsent", m -> m.computeIfAbsent(KEY, _ -> 1)), + new Operation("computeIfPresent", m -> m.computeIfPresent(KEY, (_, _) -> 1)), + new Operation("merge", m -> m.merge(KEY, KEY, (a, _) -> a)), + new Operation("put", m -> m.put(0, 0)), + new Operation("putAll", m -> m.putAll(Map.of())), + new Operation("remove1", m -> m.remove(KEY)), + new Operation("remove2", m -> m.remove(KEY, KEY)), + new Operation("replace2", m -> m.replace(KEY, 1)), + new Operation("replace3", m -> m.replace(KEY, KEY, 1)), + new Operation("replaceAll", m -> m.replaceAll((a, _) -> a)) + ); + } + + static Map newMap() { + return StableValue.map(KEYS, IDENTITY); + } + + static Map newEmptyMap() { + return StableValue.map(EMPTY, IDENTITY); + } + + static Map newRegularMap() { + return KEYS.stream().collect(Collectors.toMap(IDENTITY, IDENTITY)); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableSupplierTest.java b/test/jdk/java/lang/StableValue/StableSupplierTest.java new file mode 100644 index 00000000000..2d542fbf6ca --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableSupplierTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableSupplier methods + * @enablePreview + * @run junit StableSupplierTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableSupplierTest { + + private static final Supplier SUPPLIER = () -> 42; + + @Test + void factoryInvariants() { + assertThrows(NullPointerException.class, () -> StableValue.supplier(null)); + } + + @Test + void basic() { + basic(SUPPLIER); + basic(() -> null); + } + + void basic(Supplier supplier) { + StableTestUtil.CountingSupplier cs = new StableTestUtil.CountingSupplier<>(supplier); + var cached = StableValue.supplier(cs); + assertEquals(".unset", cached.toString()); + assertEquals(supplier.get(), cached.get()); + assertEquals(1, cs.cnt()); + assertEquals(supplier.get(), cached.get()); + assertEquals(1, cs.cnt()); + assertEquals(Objects.toString(supplier.get()), cached.toString()); + } + + @Test + void exception() { + StableTestUtil.CountingSupplier cs = new StableTestUtil.CountingSupplier<>(() -> { + throw new UnsupportedOperationException(); + }); + var cached = StableValue.supplier(cs); + assertThrows(UnsupportedOperationException.class, cached::get); + assertEquals(1, cs.cnt()); + assertThrows(UnsupportedOperationException.class, cached::get); + assertEquals(2, cs.cnt()); + assertEquals(".unset", cached.toString()); + } + + @Test + void circular() { + final AtomicReference> ref = new AtomicReference<>(); + Supplier> cached = StableValue.supplier(ref::get); + ref.set(cached); + cached.get(); + String toString = cached.toString(); + assertTrue(toString.startsWith("(this StableSupplier)")); + assertDoesNotThrow(cached::hashCode); + } + + @Test + void equality() { + Supplier f0 = StableValue.supplier(SUPPLIER); + Supplier f1 = StableValue.supplier(SUPPLIER); + // No function is equal to another function + assertNotEquals(f0, f1); + } + + @Test + void hashCodeStable() { + Supplier f0 = StableValue.supplier(SUPPLIER); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + f0.get(); + assertEquals(System.identityHashCode(f0), f0.hashCode()); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableTestUtil.java b/test/jdk/java/lang/StableValue/StableTestUtil.java new file mode 100644 index 00000000000..f71915c28ee --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableTestUtil.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, 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.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Supplier; + +final class StableTestUtil { + + private StableTestUtil() {} + + public static final class CountingSupplier + extends AbstractCounting> + implements Supplier { + + public CountingSupplier(Supplier delegate) { + super(delegate); + } + + @Override + public T get() { + incrementCounter(); + return delegate.get(); + } + + } + + public static final class CountingIntFunction + extends AbstractCounting> + implements IntFunction { + + public CountingIntFunction(IntFunction delegate) { + super(delegate); + } + + @Override + public R apply(int value) { + incrementCounter(); + return delegate.apply(value); + } + + } + + public static final class CountingFunction + extends AbstractCounting> + implements Function { + + public CountingFunction(Function delegate) { + super(delegate); + } + + @Override + public R apply(T t) { + incrementCounter(); + return delegate.apply(t); + } + + } + + public static final class CountingBiFunction + extends AbstractCounting> + implements BiFunction { + + public CountingBiFunction(BiFunction delegate) { + super(delegate); + } + + @Override + public R apply(T t, U u) { + incrementCounter(); + return delegate.apply(t, u); + } + } + + abstract static class AbstractCounting { + + private final AtomicInteger cnt = new AtomicInteger(); + protected final D delegate; + + protected AbstractCounting(D delegate) { + this.delegate = delegate; + } + + protected final void incrementCounter() { + cnt.incrementAndGet(); + } + + public final int cnt() { + return cnt.get(); + } + + @Override + public final String toString() { + return cnt.toString(); + } + } + +} diff --git a/test/jdk/java/lang/StableValue/StableValueFactoriesTest.java b/test/jdk/java/lang/StableValue/StableValueFactoriesTest.java new file mode 100644 index 00000000000..85aee0cbeec --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableValueFactoriesTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableValue factory implementations + * @modules java.base/jdk.internal.lang.stable + * @enablePreview + * @run junit StableValueFactoriesTest + */ + +import jdk.internal.lang.stable.StableUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableValueFactoriesTest { + + @Test + void array() { + assertThrows(IllegalArgumentException.class, () -> StableUtil.array(-1)); + } + +} diff --git a/test/jdk/java/lang/StableValue/StableValueTest.java b/test/jdk/java/lang/StableValue/StableValueTest.java new file mode 100644 index 00000000000..0ed68352509 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableValueTest.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for StableValue implementations + * @enablePreview + * @run junit StableValueTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class StableValueTest { + + private static final int VALUE = 42; + private static final int VALUE2 = 13; + + @Test + void trySet() { + trySet(VALUE); + trySet(null); + } + + @Test + void preSet() { + StableValue stable = StableValue.of(VALUE); + assertTrue(stable.isSet()); + assertEquals(VALUE, stable.orElseThrow()); + assertEquals(VALUE, stable.orElse(VALUE2)); + assertEquals(VALUE, stable.orElseSet(() -> VALUE2)); + assertFalse(stable.trySet(VALUE2)); + var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(VALUE2)); + assertEquals( + "The contents is already set", + e.getMessage()); + } + + void trySet(Integer initial) { + StableValue stable = StableValue.of(); + assertTrue(stable.trySet(initial)); + assertFalse(stable.trySet(null)); + assertFalse(stable.trySet(VALUE)); + assertFalse(stable.trySet(VALUE2)); + assertEquals(initial, stable.orElseThrow()); + } + + @Test + void setOrThrowValue() { + StableValue stable = StableValue.of(); + stable.setOrThrow(VALUE); + var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(VALUE2)); + assertEquals("The contents is already set", e.getMessage()); + } + + @Test + void setOrThrowNull() { + StableValue stable = StableValue.of(); + stable.setOrThrow(null); + var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(null)); + assertEquals("The contents is already set", e.getMessage()); + } + + @Test + void orElse() { + StableValue stable = StableValue.of(); + assertEquals(VALUE, stable.orElse(VALUE)); + assertNull(stable.orElse(null)); + stable.trySet(VALUE); + assertEquals(VALUE, stable.orElse(VALUE2)); + } + + @Test + void orElseThrow() { + StableValue stable = StableValue.of(); + var e = assertThrows(NoSuchElementException.class, stable::orElseThrow); + assertEquals("No contents set", e.getMessage()); + stable.trySet(VALUE); + assertEquals(VALUE, stable.orElseThrow()); + } + + @Test + void isSet() { + isSet(VALUE); + isSet(null); + } + + void isSet(Integer initial) { + StableValue stable = StableValue.of(); + assertFalse(stable.isSet()); + stable.trySet(initial); + assertTrue(stable.isSet()); + } + + @Test + void testOrElseSetSupplier() { + StableTestUtil.CountingSupplier cs = new StableTestUtil.CountingSupplier<>(() -> VALUE); + StableValue stable = StableValue.of(); + assertThrows(NullPointerException.class, () -> stable.orElseSet(null)); + assertEquals(VALUE, stable.orElseSet(cs)); + assertEquals(1, cs.cnt()); + assertEquals(VALUE, stable.orElseSet(cs)); + assertEquals(1, cs.cnt()); + } + + @Test + void testHashCode() { + StableValue stableValue = StableValue.of(); + // Should be Object::hashCode + assertEquals(System.identityHashCode(stableValue), stableValue.hashCode()); + } + + @Test + void testEquals() { + StableValue s0 = StableValue.of(); + assertNotEquals(null, s0); + StableValue s1 = StableValue.of(); + assertNotEquals(s0, s1); // Identity based + s0.setOrThrow(42); + s1.setOrThrow(42); + assertNotEquals(s0, s1); + assertNotEquals("a", s0); + StableValue null0 = StableValue.of(); + StableValue null1 = StableValue.of(); + null0.setOrThrow(null); + null1.setOrThrow(null); + assertNotEquals(null0, null1); + } + + @Test + void toStringUnset() { + StableValue stable = StableValue.of(); + assertEquals(".unset", stable.toString()); + } + + @Test + void toStringNull() { + StableValue stable = StableValue.of(); + assertTrue(stable.trySet(null)); + assertEquals("null", stable.toString()); + } + + @Test + void toStringNonNull() { + StableValue stable = StableValue.of(); + assertTrue(stable.trySet(VALUE)); + assertEquals(Objects.toString(VALUE), stable.toString()); + } + + @Test + void toStringCircular() { + StableValue> stable = StableValue.of(); + stable.trySet(stable); + String toString = assertDoesNotThrow(stable::toString); + assertEquals("(this StableValue)", toString); + assertDoesNotThrow(stable::hashCode); + assertDoesNotThrow((() -> stable.equals(stable))); + } + + @Test + void recursiveCall() { + StableValue stable = StableValue.of(); + AtomicReference> ref = new AtomicReference<>(stable); + assertThrows(IllegalStateException.class, () -> + stable.orElseSet(() -> { + ref.get().trySet(1); + return 1; + }) + ); + assertThrows(IllegalStateException.class, () -> + stable.orElseSet(() -> { + ref.get().orElseSet(() -> 1); + return 1; + }) + ); + } + + @Test + void intFunctionExample() { + final class SqrtUtil { + + private SqrtUtil() {} + + private static final int CACHED_SIZE = 10; + + private static final IntFunction SQRT = + // @link substring="intFunction" target="#intFunction(int,IntFunction)" : + StableValue.intFunction(CACHED_SIZE, StrictMath::sqrt); + + public static double sqrt(int a) { + return SQRT.apply(a); + } + } + + double sqrt9 = SqrtUtil.sqrt(9); // May eventually constant fold to 3.0 at runtime + + assertEquals(3, sqrt9); + assertThrows(IllegalArgumentException.class, () -> SqrtUtil.sqrt(16)); + } + + @Test + void intFunctionExample2() { + final class PowerOf2Util { + + private PowerOf2Util() {} + + private static final int SIZE = 6; + private static final IntFunction ORIGINAL_POWER_OF_TWO = + v -> 1 << v; + + private static final IntFunction POWER_OF_TWO = + // @link substring="intFunction" target="#intFunction(int,IntFunction)" : + StableValue.intFunction(SIZE, ORIGINAL_POWER_OF_TWO); + + public static int powerOfTwo(int a) { + return POWER_OF_TWO.apply(a); + } + } + + int pwr4 = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime + + assertEquals(16, pwr4); + assertEquals(1, PowerOf2Util.powerOfTwo(0)); + assertEquals(8, PowerOf2Util.powerOfTwo(3)); + assertEquals(32, PowerOf2Util.powerOfTwo(5)); + assertThrows(IllegalArgumentException.class, () -> PowerOf2Util.powerOfTwo(10)); + } + + @Test + void functionExample() { + + class Log2Util { + + private Log2Util() {} + + private static final Set CACHED_KEYS = + Set.of(1, 2, 4, 8, 16, 32); + private static final UnaryOperator LOG2_ORIGINAL = + i -> 31 - Integer.numberOfLeadingZeros(i); + + private static final Function LOG2_CACHED = + // @link substring="function" target="#function(Set,Function)" : + StableValue.function(CACHED_KEYS, LOG2_ORIGINAL); + + public static double log2(int a) { + if (CACHED_KEYS.contains(a)) { + return LOG2_CACHED.apply(a); + } else { + return LOG2_ORIGINAL.apply(a); + } + } + + } + + double log16 = Log2Util.log2(16); // May eventually constant fold to 4.0 at runtime + double log256 = Log2Util.log2(256); // Will not constant fold + + assertEquals(4, log16); + assertEquals(8, log256); + } + + @Test + void functionExample2() { + + class Log2Util { + + private Log2Util() {} + + private static final Set KEYS = + Set.of(1, 2, 4, 8); + private static final UnaryOperator LOG2_ORIGINAL = + i -> 31 - Integer.numberOfLeadingZeros(i); + + private static final Function LOG2 = + // @link substring="function" target="#function(Set,Function)" : + StableValue.function(KEYS, LOG2_ORIGINAL); + + public static double log2(int a) { + return LOG2.apply(a); + } + + } + + double log16 = Log2Util.log2(8); // May eventually constant fold to 3.0 at runtime + + assertEquals(3, log16); + assertThrows(IllegalArgumentException.class, () -> Log2Util.log2(3)); + } + + private static final BiPredicate, Integer> TRY_SET = StableValue::trySet; + private static final BiPredicate, Integer> SET_OR_THROW = (s, i) -> { + try { + s.setOrThrow(i); + return true; + } catch (IllegalStateException e) { + return false; + } + }; + + @Test + void raceTrySet() { + race(TRY_SET); + } + + @Test + void raceSetOrThrow() { + race(SET_OR_THROW); + } + + @Test + void raceMixed() { + race((s, i) -> switch (i % 2) { + case 0 -> TRY_SET.test(s, i); + case 1 -> SET_OR_THROW.test(s, i); + default -> fail("should not reach here"); + }); + } + + void race(BiPredicate, Integer> winnerPredicate) { + int noThreads = 10; + CountDownLatch starter = new CountDownLatch(1); + StableValue stable = StableValue.of(); + Map winners = new ConcurrentHashMap<>(); + List threads = IntStream.range(0, noThreads).mapToObj(i -> new Thread(() -> { + try { + // Ready, set ... + starter.await(); + // Here we go! + winners.put(i, winnerPredicate.test(stable, i)); + } catch (Throwable t) { + fail(t); + } + })) + .toList(); + threads.forEach(Thread::start); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + // Start the race + starter.countDown(); + threads.forEach(StableValueTest::join); + // There can only be one winner + assertEquals(1, winners.values().stream().filter(b -> b).count()); + } + + private static void join(Thread thread) { + try { + thread.join(); + } catch (InterruptedException e) { + fail(e); + } + } + +} diff --git a/test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java b/test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java new file mode 100644 index 00000000000..151d6f9c805 --- /dev/null +++ b/test/jdk/java/lang/StableValue/StableValuesSafePublicationTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for making sure StableValue publishes values safely + * @modules java.base/jdk.internal.misc + * @enablePreview + * @run junit StableValuesSafePublicationTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +final class StableValuesSafePublicationTest { + + private static final int SIZE = 100_000; + private static final int THREADS = Runtime.getRuntime().availableProcessors(); + private static final StableValue[] STABLES = stables(); + + static StableValue[] stables() { + @SuppressWarnings("unchecked") + StableValue[] stables = (StableValue[]) new StableValue[SIZE]; + for (int i = 0; i < SIZE; i++) { + stables[i] = StableValue.of(); + } + return stables; + } + + static final class Holder { + // These are non-final fields but should be seen + // fully initialized thanks to the HB properties of StableValue. + int a, b, c, d, e; + + Holder() { + a = b = c = d = e = 1; + } + } + + static final class Consumer implements Runnable { + + final int[] observations = new int[SIZE]; + final StableValue[] stables = STABLES; + int i = 0; + + @Override + public void run() { + for (; i < SIZE; i++) { + StableValue s = stables[i]; + Holder h; + // Wait until the StableValue has a holder value + while ((h = s.orElse(null)) == null) {} + int a = h.a; + int b = h.b; + int c = h.c; + int d = h.d; + int e = h.e; + observations[i] = a + (b << 1) + (c << 2) + (c << 3) + (d << 4) + (e << 5); + } + } + } + + static final class Producer implements Runnable { + + final StableValue[] stables = STABLES; + + @Override + public void run() { + StableValue s; + long deadlineNs = System.nanoTime(); + for (int i = 0; i < SIZE; i++) { + s = stables[i]; + s.trySet(new Holder()); + deadlineNs += 1000; + while (System.nanoTime() < deadlineNs) { + Thread.onSpinWait(); + } + } + } + } + + @Test + void main() { + List consumers = IntStream.range(0, THREADS) + .mapToObj(_ -> new Consumer()) + .toList(); + + List consumersThreads = IntStream.range(0, THREADS) + .mapToObj(i -> Thread.ofPlatform() + .name("Consumer Thread " + i) + .start(consumers.get(i))) + .toList(); + + Producer producer = new Producer(); + + Thread producerThread = Thread.ofPlatform() + .name("Producer Thread") + .start(producer); + + join(consumers, producerThread); + join(consumers, consumersThreads.toArray(Thread[]::new)); + + int[] histogram = new int[64]; + for (Consumer consumer : consumers) { + for (int i = 0; i < SIZE; i++) { + histogram[consumer.observations[i]]++; + } + } + + // unless a = 1, ..., e = 1, zero observations should be seen + for (int i = 0; i < 63; i++) { + assertEquals(0, histogram[i]); + } + // a = 1, ..., e = 1 : index 2^5-1 = 63 + // All observations should end up in this bucket + assertEquals(THREADS * SIZE, histogram[63]); + } + + static void join(List consumers, Thread... threads) { + try { + for (Thread t:threads) { + long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(1); + while (t.isAlive()) { + t.join(TimeUnit.SECONDS.toMillis(10)); + if (t.isAlive()) { + String stack = Arrays.stream(t.getStackTrace()) + .map(Objects::toString) + .collect(Collectors.joining(System.lineSeparator())); + System.err.println(t + ": " + stack); + for (int i = 0; i < consumers.size(); i++) { + System.err.println("Consumer " + i + ": " + consumers.get(i).i); + } + } + if (System.nanoTime() > deadline) { + long nonNulls = CompletableFuture.supplyAsync(() -> + Stream.of(STABLES) + .map(s -> s.orElse(null)) + .filter(Objects::nonNull) + .count(), Executors.newSingleThreadExecutor()).join(); + fail("Giving up! Set stables seen by a new thread: " + nonNulls); + } + } + } + } catch (InterruptedException ie) { + fail(ie); + } + } + +} diff --git a/test/jdk/java/lang/StableValue/TrustedFieldTypeTest.java b/test/jdk/java/lang/StableValue/TrustedFieldTypeTest.java new file mode 100644 index 00000000000..205e5ed3a77 --- /dev/null +++ b/test/jdk/java/lang/StableValue/TrustedFieldTypeTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* @test + * @summary Basic tests for TrustedFieldType implementations + * @modules jdk.unsupported/sun.misc + * @modules java.base/jdk.internal.lang.stable + * @modules java.base/jdk.internal.misc + * @enablePreview + * @run junit/othervm --add-opens java.base/jdk.internal.lang.stable=ALL-UNNAMED -Dopens=true TrustedFieldTypeTest + * @run junit/othervm -Dopens=false TrustedFieldTypeTest + */ + +import jdk.internal.lang.stable.StableValueImpl; +import jdk.internal.misc.Unsafe; +import org.junit.jupiter.api.Test; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; + +import static org.junit.jupiter.api.Assertions.*; + +final class TrustedFieldTypeTest { + + @Test + void varHandle() throws NoSuchFieldException, IllegalAccessException { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + StableValue originalValue = StableValue.of(); + @SuppressWarnings("unchecked") + StableValue[] originalArrayValue = new StableValue[10]; + + final class Holder { + private final StableValue value = originalValue; + } + final class ArrayHolder { + private final StableValue[] array = originalArrayValue; + } + + + VarHandle valueVarHandle = lookup.findVarHandle(Holder.class, "value", StableValue.class); + Holder holder = new Holder(); + + assertThrows(UnsupportedOperationException.class, () -> + valueVarHandle.set(holder, StableValue.of()) + ); + + assertThrows(UnsupportedOperationException.class, () -> + valueVarHandle.compareAndSet(holder, originalValue, StableValue.of()) + ); + + VarHandle arrayVarHandle = lookup.findVarHandle(ArrayHolder.class, "array", StableValue[].class); + ArrayHolder arrayHolder = new ArrayHolder(); + + assertThrows(UnsupportedOperationException.class, () -> + arrayVarHandle.set(arrayHolder, new StableValue[1]) + ); + + assertThrows(UnsupportedOperationException.class, () -> + arrayVarHandle.compareAndSet(arrayHolder, originalArrayValue, new StableValue[1]) + ); + + } + + @Test + void updateStableValueContentVia_j_i_m_Unsafe() { + StableValue stableValue = StableValue.of(); + stableValue.trySet(42); + jdk.internal.misc.Unsafe unsafe = Unsafe.getUnsafe(); + + long offset = unsafe.objectFieldOffset(stableValue.getClass(), "contents"); + assertTrue(offset > 0); + + // Unfortunately, it is possible to update the underlying data via jdk.internal.misc.Unsafe + Object oldData = unsafe.getAndSetReference(stableValue, offset, 13); + assertEquals(42, oldData); + assertEquals(13, stableValue.orElseThrow()); + } + + @Test + void updateStableValueContentViaSetAccessible() throws NoSuchFieldException, IllegalAccessException { + + if (Boolean.getBoolean("opens")) { + // Unfortunately, add-opens allows direct access to the `value` field + Field field = StableValueImpl.class.getDeclaredField("contents"); + field.setAccessible(true); + + StableValue stableValue = StableValue.of(); + stableValue.trySet(42); + + Object oldData = field.get(stableValue); + assertEquals(42, oldData); + + field.set(stableValue, 13); + assertEquals(13, stableValue.orElseThrow()); + } else { + Field field = StableValueImpl.class.getDeclaredField("contents"); + assertThrows(InaccessibleObjectException.class, ()-> field.setAccessible(true)); + } + } + +} diff --git a/test/jdk/java/util/Collection/MOAT.java b/test/jdk/java/util/Collection/MOAT.java index 1c997a4b26a..e87b071d80a 100644 --- a/test/jdk/java/util/Collection/MOAT.java +++ b/test/jdk/java/util/Collection/MOAT.java @@ -30,6 +30,7 @@ * @summary Run many tests on many Collection and Map implementations * @author Martin Buchholz * @modules java.base/java.util:open + * @enablePreview * @run main MOAT * @key randomness */ @@ -219,10 +220,15 @@ public class MOAT { // Immutable List testEmptyList(List.of()); testEmptyList(List.of().subList(0,0)); + testEmptyList(StableValue.list(0, i -> i)); + testEmptyList(StableValue.list(3, i -> i).subList(0, 0)); testListMutatorsAlwaysThrow(List.of()); testListMutatorsAlwaysThrow(List.of().subList(0,0)); + testListMutatorsAlwaysThrow(StableValue.list(0, i -> i)); testEmptyListMutatorsAlwaysThrow(List.of()); testEmptyListMutatorsAlwaysThrow(List.of().subList(0,0)); + testEmptyListMutatorsAlwaysThrow(StableValue.list(0, i -> i)); + testEmptyListMutatorsAlwaysThrow(StableValue.list(3, i -> i).subList(0, 0)); for (List list : Arrays.asList( List.of(), List.of(1), @@ -244,7 +250,10 @@ public class MOAT { Stream.of((Integer)null).toList(), Stream.of(1, null).toList(), Stream.of(1, null, 3).toList(), - Stream.of(1, null, 3, 4).toList())) { + Stream.of(1, null, 3, 4).toList(), + StableValue.list(0, i -> i), + StableValue.list(3, i -> i), + StableValue.list(10, i -> i))) { testCollection(list); testImmutableList(list); testListMutatorsAlwaysThrow(list); @@ -356,6 +365,9 @@ public class MOAT { testEmptyMap(Map.of()); testMapMutatorsAlwaysThrow(Map.of()); testEmptyMapMutatorsAlwaysThrow(Map.of()); + testEmptyMap(StableValue.map(Set.of(), k -> k)); + testMapMutatorsAlwaysThrow(StableValue.map(Set.of(), k -> k)); + testEmptyMapMutatorsAlwaysThrow(StableValue.map(Set.of(), k -> k)); for (Map map : Arrays.asList( Map.of(), Map.of(1, 101), @@ -368,7 +380,10 @@ public class MOAT { Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808), Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808, 9, 909), Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808, 9, 909, 10, 1010), - Map.ofEntries(ea))) { + Map.ofEntries(ea), + StableValue.map(Set.of(), k -> k), + StableValue.map(Set.of(1), k -> k), + StableValue.map(Set.of(1, 2, 3), k -> k))) { testMap(map); testImmutableMap(map); testMapMutatorsAlwaysThrow(map); diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionBenchmark.java new file mode 100644 index 00000000000..44fd3f2c18e --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionBenchmark.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +@OperationsPerInvocation(100) +public class StableFunctionBenchmark { + + private static final int SIZE = 100; + private static final Set SET = IntStream.range(0, SIZE).boxed().collect(Collectors.toSet()); + + private static final Map MAP = StableValue.map(SET, Function.identity()); + private static final Function FUNCTION = StableValue.function(SET, Function.identity()); + + private final Map map = StableValue.map(SET, Function.identity()); + private final Function function = StableValue.function(SET, Function.identity()); + + @Benchmark + public int map() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += map.get(i); + } + return sum; + } + + @Benchmark + public int function() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += function.apply(i); + } + return sum; + } + + @Benchmark + public int staticSMap() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += MAP.get(i); + } + return sum; + } + + @Benchmark + public int staticIntFunction() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += FUNCTION.apply(i); + } + return sum; + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionSingleBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionSingleBenchmark.java new file mode 100644 index 00000000000..1cb1a04582f --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableFunctionSingleBenchmark.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +public class StableFunctionSingleBenchmark { + + private static final int SIZE = 100; + private static final Set SET = IntStream.range(0, SIZE).boxed().collect(Collectors.toSet()); + + private static final Map MAP = StableValue.map(SET, Function.identity()); + private static final Function FUNCTION = StableValue.function(SET, Function.identity()); + + private final Map map = StableValue.map(SET, Function.identity()); + private final Function function = StableValue.function(SET, Function.identity()); + + @Benchmark + public int map() { + return map.get(1); + } + + @Benchmark + public int function() { + return function.apply(1); + } + + @Benchmark + public int staticSMap() { + return MAP.get(1); + } + + @Benchmark + public int staticIntFunction() { + return FUNCTION.apply(1); + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionBenchmark.java new file mode 100644 index 00000000000..0b8e5d97cac --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionBenchmark.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +@OperationsPerInvocation(100) +public class StableIntFunctionBenchmark { + + private static final int SIZE = 100; + private static final IntFunction IDENTITY = i -> i; + + private static final List LIST = StableValue.list(SIZE, IDENTITY); + private static final IntFunction INT_FUNCTION = StableValue.intFunction(SIZE, IDENTITY); + + private final List list = StableValue.list(SIZE, IDENTITY); + private final IntFunction intFunction = StableValue.intFunction(SIZE, IDENTITY); + + @Benchmark + public int list() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += list.get(i); + } + return sum; + } + + @Benchmark + public int intFunction() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += intFunction.apply(i); + } + return sum; + } + + @Benchmark + public int staticList() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += LIST.get(i); + } + return sum; + } + + @Benchmark + public int staticIntFunction() { + int sum = 0; + for (int i = 0; i < SIZE; i++) { + sum += INT_FUNCTION.apply(i); + } + return sum; + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionSingleBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionSingleBenchmark.java new file mode 100644 index 00000000000..1e8e250ba8a --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableIntFunctionSingleBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +public class StableIntFunctionSingleBenchmark { + + private static final int SIZE = 100; + private static final IntFunction IDENTITY = i -> i; + + private static final List STABLE = StableValue.list(SIZE, IDENTITY); + private static final IntFunction INT_FUNCTION = StableValue.intFunction(SIZE, IDENTITY); + + private final List stable = StableValue.list(SIZE, IDENTITY); + private final IntFunction intFunction = StableValue.intFunction(SIZE, IDENTITY); + + @Benchmark + public int list() { + return stable.get(1); + } + + @Benchmark + public int intFunction() { + return intFunction.apply(1); + } + + @Benchmark + public int staticList() { + return STABLE.get(1); + } + + @Benchmark + public int staticIntFunction() { + return INT_FUNCTION.apply(1); + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableMethodHandleBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableMethodHandleBenchmark.java new file mode 100644 index 00000000000..5017fb4e11a --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableMethodHandleBenchmark.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +import org.openjdk.bench.java.lang.stable.StableValueBenchmark.Dcl; +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.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.constant.ConstantDescs.*; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +public class StableMethodHandleBenchmark { + + private static final MethodHandle FINAL_MH = identityHandle(); + private static final StableValue STABLE_MH; + private static MethodHandle mh = identityHandle(); + private static final Dcl DCL = new Dcl<>(StableMethodHandleBenchmark::identityHandle); + private static final AtomicReference ATOMIC_REFERENCE = new AtomicReference<>(identityHandle()); + private static final Map MAP = new ConcurrentHashMap<>(); + private static final Map STABLE_MAP = StableValue.map(Set.of("identityHandle"), _ -> identityHandle()); + + static { + STABLE_MH = StableValue.of(); + STABLE_MH.setOrThrow(identityHandle()); + MAP.put("identityHandle", identityHandle()); + } + + @Benchmark + public int atomic() throws Throwable { + return (int) ATOMIC_REFERENCE.get().invokeExact(1); + } + + @Benchmark + public int dcl() throws Throwable { + return (int) DCL.get().invokeExact(1); + } + + @Benchmark + public int finalMh() throws Throwable { + return (int) FINAL_MH.invokeExact(1); + } + + @Benchmark + public int map() throws Throwable { + return (int) MAP.get("identityHandle").invokeExact(1); + } + + @Benchmark + public int nonFinalMh() throws Throwable { + return (int) mh.invokeExact(1); + } + + @Benchmark + public int stableMap() throws Throwable { + return (int) STABLE_MAP.get("identityHandle").invokeExact(1); + } + + @Benchmark + public int stableMh() throws Throwable { + return (int) STABLE_MH.orElseThrow().invokeExact(1); + } + + Object cp() { + CodeBuilder cob = null; + ConstantPoolBuilder cp = ConstantPoolBuilder.of(); + cob.ldc(cp.constantDynamicEntry(cp.bsmEntry(cp.methodHandleEntry(BSM_CLASS_DATA), List.of()), + cp.nameAndTypeEntry(DEFAULT_NAME, CD_MethodHandle))); + return null; + } + + static MethodHandle identityHandle() { + var lookup = MethodHandles.lookup(); + try { + return lookup.findStatic(StableMethodHandleBenchmark.class, "identity", MethodType.methodType(int.class, int.class)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static int identity(int value) { + return value; + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableSupplierBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableSupplierBenchmark.java new file mode 100644 index 00000000000..883f13da05a --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableSupplierBenchmark.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +@OperationsPerInvocation(2) +public class StableSupplierBenchmark { + + private static final int VALUE = 42; + private static final int VALUE2 = 23; + + private static final StableValue STABLE = init(StableValue.of(), VALUE); + private static final StableValue STABLE2 = init(StableValue.of(), VALUE2); + private static final Supplier SUPPLIER = StableValue.supplier(() -> VALUE); + private static final Supplier SUPPLIER2 = StableValue.supplier(() -> VALUE); + + private final StableValue stable = init(StableValue.of(), VALUE); + private final StableValue stable2 = init(StableValue.of(), VALUE2); + private final Supplier supplier = StableValue.supplier(() -> VALUE); + private final Supplier supplier2 = StableValue.supplier(() -> VALUE2); + + @Benchmark + public int stable() { + return stable.orElseThrow() + stable2.orElseThrow(); + } + + @Benchmark + public int supplier() { + return supplier.get() + supplier2.get(); + } + + @Benchmark + public int staticStable() { + return STABLE.orElseThrow() + STABLE2.orElseThrow(); + } + + @Benchmark + public int staticSupplier() { + return SUPPLIER.get() + SUPPLIER2.get(); + } + + private static StableValue init(StableValue m, Integer value) { + m.trySet(value); + return m; + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/StableValueBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/StableValueBenchmark.java new file mode 100644 index 00000000000..505cfffdc2c --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/StableValueBenchmark.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +import org.openjdk.jmh.annotations.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * Benchmark measuring StableValue performance + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) // Share the same state instance (for contention) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 2) +@Fork(value = 2, jvmArgsAppend = { + "--enable-preview" +}) +@Threads(Threads.MAX) // Benchmark under contention +@OperationsPerInvocation(2) +public class StableValueBenchmark { + + private static final int VALUE = 42; + private static final int VALUE2 = 23; + + private static final StableValue STABLE = init(StableValue.of(), VALUE); + private static final StableValue STABLE2 = init(StableValue.of(), VALUE2); + private static final StableValue DCL = init(StableValue.of(), VALUE); + private static final StableValue DCL2 = init(StableValue.of(), VALUE2); + private static final AtomicReference ATOMIC = new AtomicReference<>(VALUE); + private static final AtomicReference ATOMIC2 = new AtomicReference<>(VALUE2); + private static final Holder HOLDER = new Holder(VALUE); + private static final Holder HOLDER2 = new Holder(VALUE2); + private static final RecordHolder RECORD_HOLDER = new RecordHolder(VALUE); + private static final RecordHolder RECORD_HOLDER2 = new RecordHolder(VALUE2); + + private final StableValue stable = init(StableValue.of(), VALUE); + private final StableValue stable2 = init(StableValue.of(), VALUE2); + private final StableValue stableNull = StableValue.of(); + private final StableValue stableNull2 = StableValue.of(); + private final Supplier dcl = new Dcl<>(() -> VALUE); + private final Supplier dcl2 = new Dcl<>(() -> VALUE2); + private final AtomicReference atomic = new AtomicReference<>(VALUE); + private final AtomicReference atomic2 = new AtomicReference<>(VALUE2); + private final Supplier supplier = () -> VALUE; + private final Supplier supplier2 = () -> VALUE2; + + + @Setup + public void setup() { + stableNull.trySet(null); + stableNull2.trySet(null); + } + + @Benchmark + public int atomic() { + return atomic.get() + atomic2.get(); + } + + @Benchmark + public int dcl() { + return dcl.get() + dcl2.get(); + } + + @Benchmark + public int stable() { + return stable.orElseThrow() + stable2.orElseThrow(); + } + + @Benchmark + public int stableNull() { + return (stableNull.orElseThrow() == null ? VALUE : VALUE2) + (stableNull2.orElseThrow() == null ? VALUE : VALUE2); + } + + // Reference case + @Benchmark + public int refSupplier() { + return supplier.get() + supplier2.get(); + } + + @Benchmark + public int staticAtomic() { + return ATOMIC.get() + ATOMIC2.get(); + } + + @Benchmark + public int staticDcl() { + return DCL.orElseThrow() + DCL2.orElseThrow(); + } + + @Benchmark + public int staticHolder() { + return HOLDER.get() + HOLDER2.get(); + } + + @Benchmark + public int staticRecordHolder() { + return RECORD_HOLDER.get() + RECORD_HOLDER2.get(); + } + + @Benchmark + public int staticStable() { + return STABLE.orElseThrow() + STABLE2.orElseThrow(); + } + + + private static StableValue init(StableValue m, Integer value) { + m.trySet(value); + return m; + } + + private static final class Holder { + + private final StableValue delegate = StableValue.of(); + + Holder(int value) { + delegate.setOrThrow(value); + } + + int get() { + return delegate.orElseThrow(); + } + + } + + private record RecordHolder(StableValue delegate) { + + RecordHolder(int value) { + this(StableValue.of()); + delegate.setOrThrow(value); + } + + int get() { + return delegate.orElseThrow(); + } + + } + + + // Handles null values + public static class Dcl implements Supplier { + + private final Supplier supplier; + + private volatile V value; + private boolean bound; + + public Dcl(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public V get() { + V v = value; + if (v == null) { + if (!bound) { + synchronized (this) { + v = value; + if (v == null) { + if (!bound) { + value = v = supplier.get(); + bound = true; + } + } + } + } + } + return v; + } + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/stable/VarHandleHolderBenchmark.java b/test/micro/org/openjdk/bench/java/lang/stable/VarHandleHolderBenchmark.java new file mode 100644 index 00000000000..65d53a42e88 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/stable/VarHandleHolderBenchmark.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2025, 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.lang.stable; + +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.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.VarHandle; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.lang.foreign.MemoryLayout.PathElement.groupElement; +import static java.util.concurrent.TimeUnit.*; + +@Warmup(iterations = 5, time = 5, timeUnit = SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = SECONDS) +@Fork(value = 1, jvmArgs = { "--enable-preview" }) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@State(Scope.Benchmark) +public class VarHandleHolderBenchmark { + + private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_INT.withName("x"), + ValueLayout.JAVA_INT.withName("y") + ); + + private static final long SIZEOF = LAYOUT.byteSize(); + private static final long OFFSET_X = LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("x")); + private static final long OFFSET_Y = LAYOUT.byteOffset(groupElement("y")); + + static final class MyVarHandleLookup implements Function { + @Override + public VarHandle apply(String name) { + return LAYOUT.arrayElementVarHandle(groupElement(name)).withInvokeExactBehavior(); + } + } + + private static final Function VAR_HANDLE_FUNCTION = new MyVarHandleLookup(); + + private static final VarHandle VH_X = VAR_HANDLE_FUNCTION.apply("x"); + private static final VarHandle VH_Y = VAR_HANDLE_FUNCTION.apply("y"); + + private static final Supplier SV_X = StableValue.supplier(() -> VAR_HANDLE_FUNCTION.apply("x")); + private static final Supplier SV_Y = StableValue.supplier(() -> VAR_HANDLE_FUNCTION.apply("y")); + + private static final Map U_MAP = Map.of( + "x", VH_X, + "y", VH_Y); + + private static final Map U_MAP_ELEMENT = Map.of( + "x", LAYOUT.varHandle(groupElement("x")), + "y", LAYOUT.varHandle(groupElement("y"))); + + private static final Map S_MAP = StableValue.map( + Set.of("x", "y"), + VAR_HANDLE_FUNCTION); + + private static final Function S_FUN = StableValue.function( + Set.of("x", "y"), + VAR_HANDLE_FUNCTION); + + private static final MemorySegment confined; + static { + var array = new int[512 * (int) SIZEOF / (int) ValueLayout.JAVA_INT.byteSize()]; + var heap = MemorySegment.ofArray(array); + for(var i = 0; i < 512; i++) { + heap.set(ValueLayout.JAVA_INT, i * SIZEOF + OFFSET_X, i); + heap.set(ValueLayout.JAVA_INT, i * SIZEOF + OFFSET_Y, i); + } + confined = Arena.ofConfined().allocate(LAYOUT, 512); + confined.copyFrom(heap); + } + + @Benchmark + public int confinedVarHandleLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) VH_X.get(confined, 0L, (long) i); + var y = (int) VH_Y.get(confined, 0L, (long) i); + sum += x /*+y*/; + } + return sum; + } + + @Benchmark + public int confinedStableValueLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) SV_X.get().get(confined, 0L, (long) i); + var y = (int) SV_Y.get().get(confined, 0L, (long) i); + sum += x + y; + } + return sum; + } + + @Benchmark + public int confinedStableMapLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) S_MAP.get("x").get(confined, 0L, (long) i); + var y = (int) S_MAP.get("y").get(confined, 0L, (long) i); + sum += x + y; + } + return sum; + } + + @Benchmark + public int confinedStableMapElementLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) U_MAP_ELEMENT.get("x").get(confined, i * 8L); + var y = (int) U_MAP_ELEMENT.get("y").get(confined, i * 8L); + sum += x + y; + } + return sum; + } + + @Benchmark + public int confinedUnmodifiableMapLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) U_MAP.get("x").get(confined, 0L, (long) i); + var y = (int) U_MAP.get("y").get(confined, 0L, (long) i); + sum += x + y; + } + return sum; + } + + @Benchmark + public int confinedStableFunctionLoop() { + var sum = 0; + for (var i = 0; i < 512; i++) { + var x = (int) S_FUN.apply("x").get(confined, 0L, (long) i); + var y = (int) S_FUN.apply("y").get(confined, 0L, (long) i); + sum += x + y; + } + return sum; + } +} \ No newline at end of file