From 6d293be9a509707dbd164bbd5db72e78d9cd0b28 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Sat, 11 Apr 2020 12:21:50 +0200 Subject: [PATCH] Reactive routes - improve @Route method invocation performance --- .../web/deployment/VertxWebProcessor.java | 153 +++++++++++++++--- .../quarkus/vertx/web/DependentRouteTest.java | 51 ++++++ .../vertx/web/runtime/RouteHandler.java | 19 ++- .../vertx/web/runtime/VertxWebRecorder.java | 15 +- 4 files changed, 210 insertions(+), 28 deletions(-) create mode 100644 extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/DependentRouteTest.java diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java index 2f93353210584..7a4162993ff96 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java @@ -1,5 +1,8 @@ package io.quarkus.vertx.web.deployment; +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; + import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -15,6 +18,10 @@ import java.util.Set; import java.util.function.Function; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.spi.Context; +import javax.enterprise.context.spi.Contextual; +import javax.enterprise.context.spi.CreationalContext; import javax.inject.Singleton; import org.jboss.jandex.AnnotationInstance; @@ -29,14 +36,17 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InjectableBean; -import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.InjectableContext; +import io.quarkus.arc.InjectableReferenceProvider; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassAnnotationExclusion; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.arc.impl.CreationalContextImpl; import io.quarkus.arc.processor.AnnotationStore; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanInfo; @@ -52,8 +62,11 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.util.HashUtil; +import io.quarkus.gizmo.AssignableResultHandle; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -98,6 +111,26 @@ class VertxWebProcessor { private static final String VALUE_ORDER = "order"; private static final String SLASH = "/"; + private static final MethodDescriptor ARC_CONTAINER = MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class); + private static final MethodDescriptor ARC_CONTAINER_GET_ACTIVE_CONTEXT = MethodDescriptor.ofMethod(ArcContainer.class, + "getActiveContext", InjectableContext.class, Class.class); + private static final MethodDescriptor ARC_CONTAINER_BEAN = MethodDescriptor.ofMethod(ArcContainer.class, "bean", + InjectableBean.class, String.class); + private static final MethodDescriptor BEAN_GET_SCOPE = MethodDescriptor.ofMethod(InjectableBean.class, "getScope", + Class.class); + private static final MethodDescriptor CONTEXT_GET = MethodDescriptor.ofMethod(Context.class, "get", Object.class, + Contextual.class, + CreationalContext.class); + private static final MethodDescriptor CONTEXT_GET_IF_PRESENT = MethodDescriptor.ofMethod(Context.class, "get", Object.class, + Contextual.class); + private static final MethodDescriptor INJECTABLE_REF_PROVIDER_GET = MethodDescriptor.ofMethod( + InjectableReferenceProvider.class, + "get", Object.class, + CreationalContext.class); + private static final MethodDescriptor INJECTABLE_BEAN_DESTROY = MethodDescriptor.ofMethod(InjectableBean.class, "destroy", + void.class, Object.class, + CreationalContext.class); + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(FeatureBuildItem.VERTX_WEB); @@ -295,6 +328,12 @@ void addAdditionalRoutes( detectConflictingRoutes(matchers); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void initRouteHandlers(VertxWebRecorder recorder, BeanContainerBuildItem container) { + recorder.initHandlers(); + } + @BuildStep AnnotationsTransformerBuildItem annotationTransformer(CustomScopeAnnotationsBuildItem scopes) { return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { @@ -365,22 +404,93 @@ private String generateHandler(BeanInfo bean, MethodInfo method, ClassOutput cla ClassCreator invokerCreator = ClassCreator.builder().classOutput(classOutput).className(generatedName) .interfaces(RouteHandler.class).build(); - // The descriptor is: void invokeBean(Object context) - MethodCreator invoke = invokerCreator.getMethodCreator("invokeBean", void.class, Object.class); - // ArcContainer container = Arc.container(); - // InjectableBean handle = container().instance(bean); - // handle.get().foo(ctx); - ResultHandle containerHandle = invoke - .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); - ResultHandle beanHandle = invoke.invokeInterfaceMethod( - MethodDescriptor.ofMethod(ArcContainer.class, "bean", InjectableBean.class, String.class), - containerHandle, invoke.load(bean.getIdentifier())); - ResultHandle instanceHandle = invoke.invokeInterfaceMethod( - MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, InjectableBean.class), - containerHandle, beanHandle); - ResultHandle beanInstanceHandle = invoke - .invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); + // Initialized state + FieldCreator beanField = invokerCreator.getFieldCreator("bean", InjectableBean.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + FieldCreator contextField = null; + FieldCreator containerField = null; + if (BuiltinScope.APPLICATION.is(bean.getScope()) || BuiltinScope.SINGLETON.is(bean.getScope())) { + // Singleton and application contexts are always active and unambiguous + contextField = invokerCreator.getFieldCreator("context", InjectableContext.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + } else { + containerField = invokerCreator.getFieldCreator("container", ArcContainer.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + } + + implementInitialize(bean, invokerCreator, beanField, contextField, containerField); + implementInvoke(bean, method, invokerCreator, beanField, contextField, containerField); + + invokerCreator.close(); + return generatedName.replace('/', '.'); + } + + void implementInitialize(BeanInfo bean, ClassCreator invokerCreator, FieldCreator beanField, FieldCreator contextField, + FieldCreator containerField) { + MethodCreator initialize = invokerCreator.getMethodCreator("initialize", void.class); + ResultHandle containerHandle = initialize + .invokeStaticMethod(ARC_CONTAINER); + ResultHandle beanHandle = initialize.invokeInterfaceMethod( + ARC_CONTAINER_BEAN, + containerHandle, initialize.load(bean.getIdentifier())); + initialize.writeInstanceField(beanField.getFieldDescriptor(), initialize.getThis(), beanHandle); + if (contextField != null) { + initialize.writeInstanceField(contextField.getFieldDescriptor(), initialize.getThis(), + initialize.invokeInterfaceMethod( + ARC_CONTAINER_GET_ACTIVE_CONTEXT, + containerHandle, initialize + .invokeInterfaceMethod( + BEAN_GET_SCOPE, + beanHandle))); + } else { + initialize.writeInstanceField(containerField.getFieldDescriptor(), initialize.getThis(), containerHandle); + } + initialize.returnValue(null); + } + + void implementInvoke(BeanInfo bean, MethodInfo method, ClassCreator invokerCreator, FieldCreator beanField, + FieldCreator contextField, FieldCreator containerField) { + // The descriptor is: void invoke(RoutingContext context) + MethodCreator invoke = invokerCreator.getMethodCreator("invoke", void.class, RoutingContext.class); + ResultHandle beanHandle = invoke.readInstanceField(beanField.getFieldDescriptor(), invoke.getThis()); + AssignableResultHandle beanInstanceHandle = invoke.createVariable(Object.class); + AssignableResultHandle creationlContextHandle = invoke.createVariable(CreationalContextImpl.class); + + if (BuiltinScope.DEPENDENT.is(bean.getScope())) { + // Always create a new dependent instance + invoke.assign(creationlContextHandle, + invoke.newInstance(MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class), + beanHandle)); + invoke.assign(beanInstanceHandle, invoke.invokeInterfaceMethod( + INJECTABLE_REF_PROVIDER_GET, beanHandle, + creationlContextHandle)); + } else { + ResultHandle contextInvokeHandle; + if (contextField != null) { + contextInvokeHandle = invoke.readInstanceField(contextField.getFieldDescriptor(), invoke.getThis()); + } else { + ResultHandle containerInvokeHandle = invoke.readInstanceField(containerField.getFieldDescriptor(), + invoke.getThis()); + contextInvokeHandle = invoke.invokeInterfaceMethod( + ARC_CONTAINER_GET_ACTIVE_CONTEXT, + containerInvokeHandle, invoke + .invokeInterfaceMethod( + BEAN_GET_SCOPE, + beanHandle)); + invoke.ifNull(contextInvokeHandle).trueBranch().throwException(ContextNotActiveException.class, + "Context not active: " + bean.getScope().getDotName()); + } + // First try to obtain the bean via Context.get(bean) + invoke.assign(beanInstanceHandle, invoke.invokeInterfaceMethod(CONTEXT_GET_IF_PRESENT, contextInvokeHandle, + beanHandle)); + // If not present, try Context.get(bean,creationalContext) + BytecodeCreator doesNotExist = invoke.ifNull(beanInstanceHandle).trueBranch(); + doesNotExist.assign(beanInstanceHandle, + doesNotExist.invokeInterfaceMethod(CONTEXT_GET, contextInvokeHandle, beanHandle, + doesNotExist.newInstance( + MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class), + beanHandle))); + } ResultHandle paramHandle; MethodDescriptor methodDescriptor; @@ -404,15 +514,12 @@ private String generateHandler(BeanInfo bean, MethodInfo method, ClassOutput cla // Invoke the business method handler invoke.invokeVirtualMethod(methodDescriptor, beanInstanceHandle, paramHandle); - // handle.destroy() - destroy dependent instance afterwards + // Destroy dependent instance afterwards if (BuiltinScope.DEPENDENT.is(bean.getScope())) { - invoke.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "destroy", void.class), - instanceHandle); + invoke.invokeInterfaceMethod(INJECTABLE_BEAN_DESTROY, beanHandle, + beanInstanceHandle, creationlContextHandle); } invoke.returnValue(null); - - invokerCreator.close(); - return generatedName.replace('/', '.'); } private static String dashify(String value) { diff --git a/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/DependentRouteTest.java b/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/DependentRouteTest.java new file mode 100644 index 0000000000000..5b85bb80a1b39 --- /dev/null +++ b/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/DependentRouteTest.java @@ -0,0 +1,51 @@ +package io.quarkus.vertx.web; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.Dependent; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; + +public class DependentRouteTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(SimpleBean.class)); + + @Test + public void testSimpleRoute() { + assertFalse(SimpleBean.DESTROYED.get()); + when().get("/hello").then().statusCode(200).body(is("Hello!")); + assertTrue(SimpleBean.DESTROYED.get()); + } + + @Dependent + static class SimpleBean { + + static final AtomicBoolean DESTROYED = new AtomicBoolean(false); + + @Route(path = "/hello") + void hello(RoutingContext context) { + context.response().setStatusCode(200).end("Hello!"); + } + + @PreDestroy + void destroy() { + DESTROYED.set(true); + } + } + +} diff --git a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandler.java b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandler.java index f51f2651f221e..ae1b2c53a2159 100644 --- a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandler.java +++ b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/RouteHandler.java @@ -2,7 +2,6 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ManagedContext; -import io.quarkus.arc.runtime.BeanInvoker; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.quarkus.vertx.web.Route; import io.vertx.core.Handler; @@ -13,7 +12,19 @@ * * @see Route */ -public interface RouteHandler extends Handler, BeanInvoker { +public interface RouteHandler extends Handler { + + /** + * Initialize the handler instance before it is put into service. + */ + void initialize(); + + /** + * Invokes the route method. + * + * @param context + */ + void invoke(RoutingContext context); @Override default void handle(RoutingContext context) { @@ -23,14 +34,14 @@ default void handle(RoutingContext context) { if (user != null) { Arc.container().beanManager().fireEvent(user.getSecurityIdentity()); } - invokeBean(context); + invoke(context); } else { try { requestContext.activate(); if (user != null) { Arc.container().beanManager().fireEvent(user.getSecurityIdentity()); } - invokeBean(context); + invoke(context); } finally { requestContext.terminate(); } diff --git a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index bd655db0ebcad..8efc269d3c7f8 100644 --- a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -1,6 +1,8 @@ package io.quarkus.vertx.web.runtime; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; import io.quarkus.runtime.annotations.Recorder; @@ -13,6 +15,8 @@ @Recorder public class VertxWebRecorder { + static volatile List handlers = new ArrayList<>(); + @SuppressWarnings("unchecked") public Handler createHandler(String handlerClassName) { try { @@ -22,13 +26,22 @@ public Handler createHandler(String handlerClassName) { } Class> handlerClazz = (Class>) cl .loadClass(handlerClassName); - return handlerClazz.getDeclaredConstructor().newInstance(); + RouteHandler handler = (RouteHandler) handlerClazz.getDeclaredConstructor().newInstance(); + handlers.add(handler); + return handler; } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) { throw new IllegalStateException("Unable to create route handler: " + handlerClassName, e); } } + public void initHandlers() { + for (RouteHandler routeHandler : handlers) { + routeHandler.initialize(); + } + handlers.clear(); + } + public Function createRouteFunction(RouteMatcher matcher, Handler bodyHandler) { return new Function() {