Skip to content

Commit

Permalink
Qute: reflection fallback value resolver optimization
Browse files Browse the repository at this point in the history
- add ValueResolver#getCachedResolver() so that reflection value
resolver can optimize the cached resolver and save two concurrent hash
map lookups and 2 MemberKey instances
  • Loading branch information
mkouba committed Sep 16, 2024
1 parent 586592e commit 4449c78
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ private CompletionStage<Object> resolve(EvalContextImpl evalContext, Iterator<Va

if (tryCachedResolver) {
// Try the cached resolver first
ValueResolver cachedResolver = evalContext.getCachedResolver();
if (cachedResolver != null && cachedResolver.appliesTo(evalContext)) {
return cachedResolver.resolve(evalContext).thenCompose(r -> {
ValueResolver cached = evalContext.getCachedResolver();
if (cached != null && cached.appliesTo(evalContext)) {
return cached.resolve(evalContext).thenCompose(r -> {
if (Results.isNotFound(r)) {
return resolve(evalContext, null, false, expression, isLastPart, partIndex);
} else {
Expand Down Expand Up @@ -214,7 +214,7 @@ private CompletionStage<Object> resolve(EvalContextImpl evalContext, Iterator<Va
return resolve(evalContext, remainingResolvers, false, expression, isLastPart, partIndex);
} else {
// Cache the first resolver where a result is found
evalContext.setCachedResolver(foundResolver);
evalContext.setCachedResolver(foundResolver.getCachedResolver(evalContext));
return CompletionStageSupport.toCompletionStage(r);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -46,9 +47,8 @@ public boolean appliesTo(EvalContext context) {
@Override
public CompletionStage<Object> resolve(EvalContext context) {
Object base = context.getBase();
MemberKey key = MemberKey.from(context);
// At this point the candidate for the given key should be already computed
AccessorCandidate candidate = candidates.get(key).orElse(null);
AccessorCandidate candidate = candidates.get(MemberKey.from(context)).orElse(null);
if (candidate == null) {
return Results.notFound(context);
}
Expand All @@ -59,6 +59,14 @@ public CompletionStage<Object> resolve(EvalContext context) {
return accessor.getValue(base);
}

@Override
public ValueResolver getCachedResolver(EvalContext context) {
// The value must be computed and the accessor must exist
AccessorCandidate candidate = candidates.get(MemberKey.from(context)).orElseThrow();
ValueAccessor accessor = candidate.getAccessor(context);
return new CachedAccessorResolver(context.getBase().getClass(), accessor);
}

public void clearCache() {
candidates.clear();
}
Expand Down Expand Up @@ -202,4 +210,26 @@ static String decapitalize(String name) {
return new String(chars);
}

class CachedAccessorResolver implements ValueResolver {

private final Class<?> matchedClass;
private final ValueAccessor accessor;

private CachedAccessorResolver(Class<?> matchedClass, ValueAccessor accessor) {
this.matchedClass = Objects.requireNonNull(matchedClass);
this.accessor = Objects.requireNonNull(accessor);
}

@Override
public boolean appliesTo(EvalContext context) {
return ValueResolver.matchClass(context, matchedClass);
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
return accessor.getValue(context.getBase());
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ default boolean appliesTo(EvalContext context) {
return true;
}

/**
* When {@link #appliesTo(EvalContext)} returns {@code true} for a specific {@link EvalContext} and the subsequent
* invocation of {@link #resolve(EvalContext)} does not return {@link Results#NotFound} the value resolver returned from
* this method is cached for the specific part of an expression.
* <p>
* By default, the resolver itself is cached. However, it is also possible to return an optimized version.
*
* @param context
* @return the resolver that should be cached
*/
default ValueResolver getCachedResolver(EvalContext context) {
return this;
}

/**
*
* @return a new builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package io.quarkus.qute;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;
import java.util.TreeMap;
import java.util.stream.IntStream;

import org.junit.jupiter.api.Test;

import io.quarkus.qute.ExpressionImpl.PartImpl;

public class ReflectionResolverTest {

@Test
Expand Down Expand Up @@ -49,6 +54,18 @@ public void testStaticMembersIgnored() {
.parse("{foo.bar ?: 'baz'}::{foo.BAR ?: 'baz'}").data("foo", new Foo("box")).render());
}

@Test
public void testCachedResolver() {
Template template = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver()).build()
.parse("{foo.name}");
Expression exp = template.findExpression(e -> e.toOriginalString().equals("foo.name"));
PartImpl part = (PartImpl) exp.getParts().get(1);
assertNull(part.cachedResolver);
assertEquals("box", template.data("foo", new Foo("box")).render());
assertNotNull(part.cachedResolver);
assertTrue(part.cachedResolver instanceof ReflectionValueResolver.CachedAccessorResolver);
}

public static class Foo {

public final String name;
Expand Down

0 comments on commit 4449c78

Please sign in to comment.