diff --git a/benchmarks/src/jmh/java/io/micronaut/core/util/memo/MemoBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/core/util/memo/MemoBenchmark.java new file mode 100644 index 00000000000..6e84bb3de27 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/core/util/memo/MemoBenchmark.java @@ -0,0 +1,123 @@ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.beans.BeanProperty; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.TimeUnit; + +@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@Fork(1) +@State(Scope.Benchmark) +public class MemoBenchmark { + private static final MemoizedFlag ANNOTATED_FLAG = + AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasAnnotation(Ann1.class)); + + @Param({"Bare", "Extra1", "Extra2"}) + String type; + @Param({"true", "false"}) + boolean annotated; + + private BeanProperty property; + + @Setup + public void setup() throws Throwable { + BeanIntrospection introspection = BeanIntrospector.SHARED.getIntrospection(Class.forName(MemoBenchmark.class.getName() + "$" + type)); + property = introspection.getProperty(annotated ? "annotatedField" : "notAnnotatedField").orElseThrow(); + + if (memoized() != annotated) { + throw new AssertionError(); + } + if (memoizedFallback() != annotated) { + throw new AssertionError(); + } + if (direct() != annotated) { + throw new AssertionError(); + } + } + + @Benchmark + public boolean memoized() { + return ANNOTATED_FLAG.get(property); + } + + @Benchmark + public boolean memoizedFallback() { + // this is the default implementation of get() + return ANNOTATED_FLAG.compute(property); + } + + @Benchmark + public boolean direct() { + return property.hasAnnotation(Ann1.class); + } + + public static void main(String[] args) throws Throwable { + MemoBenchmark memoBenchmark = new MemoBenchmark(); + memoBenchmark.type = "Bare"; + memoBenchmark.annotated = true; + memoBenchmark.setup(); + memoBenchmark.memoizedFallback(); + + Options opt = new OptionsBuilder() + .include(MemoBenchmark.class.getName() + ".*") + //.addProfiler(LinuxPerfAsmProfiler.class) + .build(); + + new Runner(opt).run(); + } + + @Introspected + record Bare( + @Ann1 String annotatedField, + String notAnnotatedField + ) { + } + + @Introspected + record Extra1( + @Ann1 @Ann2 String annotatedField, + @Ann2 String notAnnotatedField + ) { + } + + @Introspected + record Extra2( + @Ann1 @Ann2 @Ann3 String annotatedField, + @Ann2 @Ann3 String notAnnotatedField + ) { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Ann1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Ann2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Ann3 { + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 50d093566f9..4527c0f5b66 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { compileOnly(libs.graal) compileOnly(libs.managed.kotlin.stdlib) compileOnly(libs.managed.netty.common) + testImplementation(libs.junit.jupiter.params) } spotless { diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java index 8ce08266e6f..ecab37a0b07 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadata.java @@ -22,6 +22,8 @@ import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.memo.Memoizer; +import io.micronaut.core.util.memo.MemoizerNamespace; import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; @@ -55,12 +57,17 @@ * @author Graeme Rocher * @since 1.0 */ -public interface AnnotationMetadata extends AnnotationSource { +public interface AnnotationMetadata extends AnnotationSource, Memoizer { /** * A constant for representing empty metadata. */ AnnotationMetadata EMPTY_METADATA = new EmptyAnnotationMetadata(); + /** + * Namespace to use for memoized field access. See {@link Memoizer}. + */ + MemoizerNamespace MEMOIZER_NAMESPACE = MemoizerNamespace.create(); + /** * The default {@code value()} member. */ diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java index d63004f715e..2723fca6a2d 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationMetadataDelegate.java @@ -16,10 +16,17 @@ package io.micronaut.core.annotation; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.memo.MemoizerDelegate; import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Set; /** * An interface that can be implemented by other classes that delegate the resolution of the {@link AnnotationMetadata} @@ -28,7 +35,7 @@ * @author Graeme Rocher * @since 1.0 */ -public interface AnnotationMetadataDelegate extends AnnotationMetadataProvider, AnnotationMetadata { +public interface AnnotationMetadataDelegate extends AnnotationMetadataProvider, AnnotationMetadata, MemoizerDelegate { @Override default Set getStereotypeAnnotationNames() { @@ -675,4 +682,10 @@ default AnnotationMetadata copyAnnotationMetadata() { default AnnotationMetadata getTargetAnnotationMetadata() { return getAnnotationMetadata().getTargetAnnotationMetadata(); } + + @Override + @Internal + default AnnotationMetadata getMemoizerDelegate() { + return getAnnotationMetadata(); + } } diff --git a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java index 55cfc676e0d..2d7018a470b 100644 --- a/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java +++ b/core/src/main/java/io/micronaut/core/annotation/EmptyAnnotationMetadata.java @@ -18,10 +18,20 @@ import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.memo.AbstractMemoizer; +import io.micronaut.core.util.memo.MemoizerNamespace; import io.micronaut.core.value.OptionalValues; + import java.lang.annotation.Annotation; import java.lang.reflect.Array; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Set; /** * An empty representation of {@link AnnotationMetadata}. @@ -30,7 +40,11 @@ * @since 1.0 */ @Internal -final class EmptyAnnotationMetadata implements AnnotationMetadata { +final class EmptyAnnotationMetadata extends AbstractMemoizer implements AnnotationMetadata { + @Override + protected MemoizerNamespace getMemoizerNamespace() { + return MEMOIZER_NAMESPACE; + } @Override public boolean hasPropertyExpressions() { diff --git a/core/src/main/java/io/micronaut/core/util/memo/AbstractMemoizer.java b/core/src/main/java/io/micronaut/core/util/memo/AbstractMemoizer.java new file mode 100644 index 00000000000..769e72761bb --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/AbstractMemoizer.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.ArrayUtils; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +/** + * Implementation of {@link Memoizer} with backing storage. + * + * @param This memoizer type + * @author Jonas Konrad + * @since 4.10.0 + */ +@Internal +public abstract class AbstractMemoizer> implements Memoizer { + private static final Object NULL_SENTINEL = new Object(); + private static final VarHandle ITEMS_ENTRY = MethodHandles.arrayElementVarHandle(Object[].class).withInvokeExactBehavior(); + + private Object[] items = ArrayUtils.EMPTY_OBJECT_ARRAY; + private long flags; + + /** + * Get the namespace that should be used for this memoizer. This is a method instead of a field + * to save on memory. This namespace is only spot-checked on field population. + * + * @return The namespace + */ + @NonNull + @Internal + protected abstract MemoizerNamespace getMemoizerNamespace(); + + /** + * Clear all memoized values, for use when this instance is modified and memoization may now + * return a different value. This is a cheap operation. + */ + @Internal + protected final void clearMemoized() { + items = ArrayUtils.EMPTY_OBJECT_ARRAY; + flags = 0; + } + + @SuppressWarnings("unchecked") + final R getMemoized(MemoizedReference reference) { + Object[] items = this.items; + int index = reference.index; + if (index >= items.length) { + items = ensureCapacity(items, index); + } + Object item = ITEMS_ENTRY.getAcquire(items, index); + if (item == null) { + item = computeItem(reference, items, index); + } + if (item == NULL_SENTINEL) { + item = null; + } + return (R) item; + } + + private Object[] ensureCapacity(Object[] items, int accessedIndex) { + Object[] newItems = new Object[accessedIndex + 1]; + for (int i = 0; i < items.length; i++) { + ITEMS_ENTRY.setRelease(newItems, i, ITEMS_ENTRY.getAcquire(items, i)); + } + this.items = newItems; + return newItems; + } + + @SuppressWarnings("unchecked") + private Object computeItem(MemoizedReference reference, Object[] items, int index) { + if (reference.namespace != getMemoizerNamespace()) { + throw new IllegalStateException("Reference not in right namespace"); + } + Object item = reference.compute.apply((M) this); + if (item == null) { + item = NULL_SENTINEL; + } + ITEMS_ENTRY.setRelease(items, index, item); + return item; + } + + final boolean getMemoized(@NonNull MemoizedFlag flag) { + if (!(flag instanceof MemoizedFlag.InBitmask ib)) { + return getMemoizedViaReference(flag); + } + long flags = this.flags; + if ((flags & ib.maskIsSet) == 0) { + flags = computeFlag(flags, ib); + } + return (flags & ib.maskValue) != 0; + } + + private boolean getMemoizedViaReference(MemoizedFlag flag) { + return getMemoized(((MemoizedFlag.InReference) flag).reference); + } + + @SuppressWarnings("unchecked") + private long computeFlag(long flags, MemoizedFlag.InBitmask ib) { + flags |= ib.maskIsSet; + if (ib.compute.test((M) this)) { + flags |= ib.maskValue; + } else { + flags &= ~ib.maskValue; + } + this.flags = flags; + return flags; + } +} diff --git a/core/src/main/java/io/micronaut/core/util/memo/MemoizedFlag.java b/core/src/main/java/io/micronaut/core/util/memo/MemoizedFlag.java new file mode 100644 index 00000000000..0533aa69fe6 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/MemoizedFlag.java @@ -0,0 +1,104 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.NonNull; + +import java.util.function.Predicate; + +/** + * Store a boolean value in a {@link Memoizer} object. + * + * @param The memoized object type + * @author Jonas Konrad + * @since 4.10.0 + */ +@SuppressWarnings("unused") +public abstract sealed class MemoizedFlag> { + private MemoizedFlag() { + } + + abstract boolean compute(M memoizer); + + /** + * Get the memoized boolean value. + * + * @param memoizer The memoizer to load or compute this field from + * @return The memoized value + * @implNote The default implementation does no storage and computes the memoized value each time. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public boolean get(@NonNull M memoizer) { + if (memoizer instanceof AbstractMemoizer am) { + return am.getMemoized(this); + } else { + return fallback(memoizer); + } + } + + @SuppressWarnings({"unchecked"}) + private boolean fallback(@NonNull M memoizer) { + if (memoizer instanceof MemoizerDelegate md) { + return get((M) md.getMemoizerDelegate()); + } else { + return compute(memoizer); + } + } + + /** + * Implementation that stores in the {@code long} bitmask. + * + * @param The memoized object type + */ + static final class InBitmask> extends MemoizedFlag { + final MemoizerNamespace namespace; + final Predicate compute; + final long maskIsSet; + final long maskValue; + + InBitmask(MemoizerNamespace namespace, Predicate compute, int index) { + this.namespace = namespace; + this.compute = compute; + assert index < 32; + maskIsSet = (1L << (index * 2)); + maskValue = (1L << (index * 2 + 1)); + } + + @Override + boolean compute(M memoizer) { + return compute.test(memoizer); + } + } + + /** + * Implementation that stores using {@link MemoizedReference}. This should be avoided, but + * exists as a fallback when there are too many flags in a namespace. + * + * @param The memoized object type + */ + static final class InReference> extends MemoizedFlag { + final MemoizedReference reference; + + InReference(MemoizedReference reference) { + this.reference = reference; + } + + @Override + boolean compute(M memoizer) { + return reference.compute.apply(memoizer); + } + } +} diff --git a/core/src/main/java/io/micronaut/core/util/memo/MemoizedReference.java b/core/src/main/java/io/micronaut/core/util/memo/MemoizedReference.java new file mode 100644 index 00000000000..cfebf619b25 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/MemoizedReference.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.NonNull; + +import java.util.function.Function; + +/** + * Store a reference value in a {@link Memoizer} object. + * + * @param The memoized object type + * @param The stored field type + * @author Jonas Konrad + * @since 4.10.0 + */ +public final class MemoizedReference, R> { + final MemoizerNamespace namespace; + final Function compute; + final int index; + + MemoizedReference(MemoizerNamespace namespace, Function compute, int index) { + this.namespace = namespace; + this.compute = compute; + this.index = index; + } + + /** + * Get a memoized reference value. + * + * @param memoizer The memoizer to load this field from + * @return The memoized value + * @implNote The default implementation does no storage and computes the memoized value each time. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public R get(@NonNull M memoizer) { + if (memoizer instanceof AbstractMemoizer am) { + return (R) am.getMemoized(this); + } else { + return fallback(memoizer); + } + } + + @SuppressWarnings({"unchecked"}) + private R fallback(@NonNull M memoizer) { + if (memoizer instanceof MemoizerDelegate md) { + return get((M) md.getMemoizerDelegate()); + } else { + return compute.apply(memoizer); + } + } +} diff --git a/core/src/main/java/io/micronaut/core/util/memo/Memoizer.java b/core/src/main/java/io/micronaut/core/util/memo/Memoizer.java new file mode 100644 index 00000000000..fc755e41942 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/Memoizer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.Experimental; + +/** + * An object that can optionally store additional caller-customizable memoized data. Essentially, + * a memoizer contains a {@code Map}, and {@link MemoizedReference#get(Memoizer)} does a + * {@code computeIfAbsent} on that map. + * + *

Each {@link Memoizer} type is associated with a {@link MemoizerNamespace}. The + * namespace keeps track of the fields ({@link MemoizedReference}s and {@link MemoizedFlag}s) that + * are permitted for that namespace. + * + *

The namespace allows for the {@link Memoizer} implementation to be more efficient than a + * simple {@link java.util.Map}, e.g. by storing booleans as a bit field. + * + *

To avoid interface calls, the actual accessor methods are on {@link MemoizedReference} and {@link MemoizedFlag}, + * and not part of this interface. + * + * @param The type of the memoization namespace + * @since 4.10.0 + * @author Jonas Konrad + */ +@SuppressWarnings("unused") +@Experimental +public interface Memoizer> { +} diff --git a/core/src/main/java/io/micronaut/core/util/memo/MemoizerDelegate.java b/core/src/main/java/io/micronaut/core/util/memo/MemoizerDelegate.java new file mode 100644 index 00000000000..dca4ed83482 --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/MemoizerDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +/** + * Special memoizer that should delegate to another. This is used for + * {@link io.micronaut.core.annotation.AnnotationMetadataDelegate}. Generally try to avoid this, as + * it can lead to unnecessary interface calls. + * + * @param The memoizer type + * @since 4.10.0 + * @author Jonas Konrad + */ +@Internal +public interface MemoizerDelegate> extends Memoizer { + /** + * The delegate to use for storage. + * + * @return The delegate + */ + @NonNull + M getMemoizerDelegate(); +} diff --git a/core/src/main/java/io/micronaut/core/util/memo/MemoizerNamespace.java b/core/src/main/java/io/micronaut/core/util/memo/MemoizerNamespace.java new file mode 100644 index 00000000000..4fab3c8f8bc --- /dev/null +++ b/core/src/main/java/io/micronaut/core/util/memo/MemoizerNamespace.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.util.memo; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A namespace for a {@link Memoizer}. Each memoizer type is associated with exactly one + * namespace. For example, {@link io.micronaut.core.annotation.AnnotationMetadata} has the + * namespace {@link io.micronaut.core.annotation.AnnotationMetadata#MEMOIZER_NAMESPACE}. + * + *

The namespace can be used to create new "fields" that are then memoized in the + * {@link Memoizer} instances. Note that fields should be used sparingly, as each field may use + * some memory in all {@link Memoizer} instances in the namespace, not just those where the + * field is actually used. + * + * @param The memoizer type + * @author Jonas Konrad + * @since 4.10.0 + */ +public final class MemoizerNamespace> { + private final AtomicInteger nextReference = new AtomicInteger(); + private final AtomicInteger nextFlag = new AtomicInteger(); + + private MemoizerNamespace() { + } + + /** + * Create a new namespace. There should be exactly one namespace per {@link Memoizer} type. + * + *

Note: This is internal to micronaut-core for the moment.

+ * + * @param The memoizer type + * @return The namespace + */ + @Internal + @NonNull + public static > MemoizerNamespace create() { + return new MemoizerNamespace<>(); + } + + /** + * Create a new memoizer field that can store an arbitrary reference type. + * + * @param compute The function to compute the reference value + * @param The field type + * @return The field + */ + @NonNull + public MemoizedReference newReference(@NonNull Function compute) { + return new MemoizedReference<>(this, compute, nextReference.getAndIncrement()); + } + + /** + * Create a new memoizer field that can store a boolean flag. + * + * @param compute The function to compute the boolean value + * @return The field + */ + @NonNull + public MemoizedFlag newFlag(@NonNull Predicate compute) { + int index = nextFlag.getAndIncrement(); + if (index >= 32) { + return new MemoizedFlag.InReference<>(newReference(compute::test)); + } else { + return new MemoizedFlag.InBitmask<>(this, compute, index); + } + } +} diff --git a/core/src/test/java/io/micronaut/core/util/memo/MemoizedTest.java b/core/src/test/java/io/micronaut/core/util/memo/MemoizedTest.java new file mode 100644 index 00000000000..310b19bdd60 --- /dev/null +++ b/core/src/test/java/io/micronaut/core/util/memo/MemoizedTest.java @@ -0,0 +1,99 @@ +package io.micronaut.core.util.memo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MemoizedTest { + @Test + public void referenceStored() { + MemoizedReference ref = MyClass.NS.newReference(m -> { + m.getCount++; + return m.foo; + }); + + MyClass cl = new MyClass("bar"); + assertEquals(0, cl.getCount); + assertEquals("bar", ref.get(cl)); + assertEquals(1, cl.getCount); + assertEquals("bar", ref.get(cl)); + assertEquals(1, cl.getCount); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void flagStored(boolean value) { + MemoizedFlag flag = MyClass.NS.newFlag(m -> { + m.getCount++; + return value; + }); + + MyClass cl = new MyClass("bar"); + assertEquals(0, cl.getCount); + assertEquals(value, cl.getMemoized(flag)); + assertEquals(1, cl.getCount); + assertEquals(value, cl.getMemoized(flag)); + assertEquals(1, cl.getCount); + } + + @Test + public void manyFlagsStored() { + class Cl extends AbstractMemoizer { + // isolated namespace for each instance + final MemoizerNamespace ns = MemoizerNamespace.create(); + + @Override + protected MemoizerNamespace getMemoizerNamespace() { + return ns; + } + } + + Cl cl = new Cl(); + List> flags = new ArrayList<>(); + BitSet set = new BitSet(); + List getCounts = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + set.set(i, ThreadLocalRandom.current().nextBoolean()); + getCounts.add(0); + int index = i; + flags.add(cl.ns.newFlag(c -> { + getCounts.set(index, getCounts.get(index) + 1); + return set.get(index); + })); + } + + assertTrue(getCounts.stream().allMatch(i -> i == 0)); + for (int i = 0; i < flags.size(); i++) { + assertEquals(set.get(i), cl.getMemoized(flags.get(i))); + } + assertTrue(getCounts.stream().allMatch(i -> i == 1)); + for (int i = 0; i < flags.size(); i++) { + assertEquals(set.get(i), cl.getMemoized(flags.get(i))); + } + assertTrue(getCounts.stream().allMatch(i -> i == 1)); + } + + static class MyClass extends AbstractMemoizer { + static final MemoizerNamespace NS = MemoizerNamespace.create(); + + final String foo; + int getCount; + + MyClass(String foo) { + this.foo = foo; + } + + @Override + protected MemoizerNamespace getMemoizerNamespace() { + return NS; + } + } +} diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java index 9948a650803..30f5d7a7f8b 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadata.java @@ -19,14 +19,19 @@ import io.micronaut.core.annotation.AnnotationUtil; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.memo.AbstractMemoizer; +import io.micronaut.core.util.memo.MemoizerNamespace; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; import java.lang.annotation.Annotation; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static io.micronaut.core.annotation.AnnotationUtil.ZERO_ANNOTATIONS; @@ -38,7 +43,7 @@ * @since 1.0 */ @Internal -abstract class AbstractAnnotationMetadata implements AnnotationMetadata { +abstract class AbstractAnnotationMetadata extends AbstractMemoizer implements AnnotationMetadata { private volatile Map annotationMap; private volatile Map declaredAnnotationMap; @@ -51,6 +56,11 @@ abstract class AbstractAnnotationMetadata implements AnnotationMetadata { protected AbstractAnnotationMetadata() { } + @Override + protected final MemoizerNamespace getMemoizerNamespace() { + return MEMOIZER_NAMESPACE; + } + private Map getAnnotationMap() { Map map = annotationMap; if (map == null) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java index 40188879cd2..5980e2eb64f 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/AnnotationMetadataHierarchy.java @@ -24,6 +24,8 @@ import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; +import io.micronaut.core.util.memo.AbstractMemoizer; +import io.micronaut.core.util.memo.MemoizerNamespace; import io.micronaut.core.value.OptionalValues; import java.lang.annotation.Annotation; @@ -58,7 +60,7 @@ * @author graemerocher * @since 1.3.0 */ -public final class AnnotationMetadataHierarchy implements AnnotationMetadata, EnvironmentAnnotationMetadata, Iterable { +public final class AnnotationMetadataHierarchy extends AbstractMemoizer implements AnnotationMetadata, EnvironmentAnnotationMetadata, Iterable { /** * Constant to represent an empty hierarchy. */ @@ -106,6 +108,11 @@ private AnnotationMetadataHierarchy(AnnotationMetadata[] existing, AnnotationMet delegateDeclaredToAllElements = false; } + @Override + protected @NonNull MemoizerNamespace getMemoizerNamespace() { + return MEMOIZER_NAMESPACE; + } + @Override public boolean hasPropertyExpressions() { for (AnnotationMetadata annotationMetadata : hierarchy) { diff --git a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java index 2ffbe4167a3..f9f15fc1d33 100644 --- a/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java +++ b/inject/src/main/java/io/micronaut/inject/annotation/MutableAnnotationMetadata.java @@ -18,10 +18,10 @@ import io.micronaut.context.env.DefaultPropertyPlaceholderResolver; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; -import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.expressions.EvaluatedExpressionReference; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; @@ -293,6 +293,7 @@ public final void addDefaultAnnotationValues(String annotation, Map void removeAnnotationIf(@NonNull Predicate> predicate) { removeAnnotationsIf(predicate, this.declaredAnnotations); removeAnnotationsIf(predicate, this.allAnnotations); + clearMemoized(); } private void removeAnnotationsIf(@NonNull Predicate> predicate, Map> annotations) { @@ -1001,6 +1007,7 @@ public void removeAnnotation(String annotationType) { if (annotationRepeatableContainer != null) { annotationRepeatableContainer.remove(annotationType); } + clearMemoized(); } /** @@ -1030,6 +1037,7 @@ public void removeStereotype(String annotationType) { i.remove(); } } + clearMemoized(); } private void removeFromStereotypes(String annotationType) {