Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions benchmarks/src/jmh/java/io/micronaut/core/util/memo/MemoBenchmark.java
Original file line number Diff line number Diff line change
@@ -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<AnnotationMetadata> 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 {
}
}
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
compileOnly(libs.graal)
compileOnly(libs.managed.kotlin.stdlib)
compileOnly(libs.managed.netty.common)
testImplementation(libs.junit.jupiter.params)
}

spotless {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,12 +57,17 @@
* @author Graeme Rocher
* @since 1.0
*/
public interface AnnotationMetadata extends AnnotationSource {
public interface AnnotationMetadata extends AnnotationSource, Memoizer<AnnotationMetadata> {
/**
* A constant for representing empty metadata.
*/
AnnotationMetadata EMPTY_METADATA = new EmptyAnnotationMetadata();

/**
* Namespace to use for memoized field access. See {@link Memoizer}.
*/
MemoizerNamespace<AnnotationMetadata> MEMOIZER_NAMESPACE = MemoizerNamespace.create();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add javadoc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutable data structure in a static field?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's mutable, that's why it's important to only create a limited number of MemoizedReferences.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the concern is that users could fiddle with this mutable static fields and create weird bugs


/**
* The default {@code value()} member.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -28,7 +35,7 @@
* @author Graeme Rocher
* @since 1.0
*/
public interface AnnotationMetadataDelegate extends AnnotationMetadataProvider, AnnotationMetadata {
public interface AnnotationMetadataDelegate extends AnnotationMetadataProvider, AnnotationMetadata, MemoizerDelegate<AnnotationMetadata> {

@Override
default Set<String> getStereotypeAnnotationNames() {
Expand Down Expand Up @@ -675,4 +682,10 @@ default AnnotationMetadata copyAnnotationMetadata() {
default AnnotationMetadata getTargetAnnotationMetadata() {
return getAnnotationMetadata().getTargetAnnotationMetadata();
}

@Override
@Internal
default AnnotationMetadata getMemoizerDelegate() {
return getAnnotationMetadata();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -30,7 +40,11 @@
* @since 1.0
*/
@Internal
final class EmptyAnnotationMetadata implements AnnotationMetadata {
final class EmptyAnnotationMetadata extends AbstractMemoizer<AnnotationMetadata> implements AnnotationMetadata {
@Override
protected MemoizerNamespace<AnnotationMetadata> getMemoizerNamespace() {
return MEMOIZER_NAMESPACE;
}

@Override
public boolean hasPropertyExpressions() {
Expand Down
125 changes: 125 additions & 0 deletions core/src/main/java/io/micronaut/core/util/memo/AbstractMemoizer.java
Original file line number Diff line number Diff line change
@@ -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 <M> This memoizer type
* @author Jonas Konrad
* @since 4.10.0
*/
@Internal
public abstract class AbstractMemoizer<M extends Memoizer<M>> implements Memoizer<M> {
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<M> 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> R getMemoized(MemoizedReference<M, R> 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<M, ?> 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<M> flag) {
if (!(flag instanceof MemoizedFlag.InBitmask<M> 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<M> flag) {
return getMemoized(((MemoizedFlag.InReference<M>) flag).reference);
}

@SuppressWarnings("unchecked")
private long computeFlag(long flags, MemoizedFlag.InBitmask<M> ib) {
flags |= ib.maskIsSet;
if (ib.compute.test((M) this)) {
flags |= ib.maskValue;
} else {
flags &= ~ib.maskValue;
}
this.flags = flags;
return flags;
}
}
Loading
Loading