Skip to content

Commit

Permalink
Merge pull request #8536 from mkouba/vertx-web-route-perf
Browse files Browse the repository at this point in the history
Reactive routes - improve @route method invocation performance
  • Loading branch information
mkouba authored Apr 14, 2020
2 parents fcf329b + 6d293be commit 06dc695
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Foo: bean = container.bean("1");
// InstanceHandle<Foo> 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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,7 +12,19 @@
*
* @see Route
*/
public interface RouteHandler extends Handler<RoutingContext>, BeanInvoker<RoutingContext> {
public interface RouteHandler extends Handler<RoutingContext> {

/**
* 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) {
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +15,8 @@
@Recorder
public class VertxWebRecorder {

static volatile List<RouteHandler> handlers = new ArrayList<>();

@SuppressWarnings("unchecked")
public Handler<RoutingContext> createHandler(String handlerClassName) {
try {
Expand All @@ -22,13 +26,22 @@ public Handler<RoutingContext> createHandler(String handlerClassName) {
}
Class<? extends Handler<RoutingContext>> handlerClazz = (Class<? extends Handler<RoutingContext>>) 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<Router, io.vertx.ext.web.Route> createRouteFunction(RouteMatcher matcher,
Handler<RoutingContext> bodyHandler) {
return new Function<Router, io.vertx.ext.web.Route>() {
Expand Down

0 comments on commit 06dc695

Please sign in to comment.