Simple manometry

Test parameters

The JVM parameter

-Xmx300m -Xms300m

JDK for Build 17-LOOM +7-342 (2021/5/11)

Sleep 1 s

loom

@RequestMapping("/hello")
    public DeferredResult<String> hello(a){
        DeferredResult<String> res = new DeferredResult<>();
        Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            res.setResult("hello");
        });
        return res;
    }
Copy the code
The loom

@RequestMapping("/word")
    public String word(a) throws InterruptedException {
        Thread.sleep(1000);
        return "hello";
    }
Copy the code
Single-threaded VerTX + KT coroutine

class ClientVerticle :  CoroutineVerticle(){
  override suspend fun start() {
    var router = Router.router(vertx)
    router.get("/word")
      .handler { rc->
        rc.vertx().setTimer(1000){
          rc.end("word")
        }
      }
    val httpserver = vertx.createHttpServer()
      .requestHandler(router)
      .listen(8080)
      .await()
    println("http server run on ${httpserver.actualPort()}")
  }
}
​
Copy the code
conclusion

Loom is about three or four times as good as Vertx’s EventLoop but still uses four to six times as much memory as Vertx

At present, Loom does not cause too much pressure on GC or memory allocation. Instead, it is better to use simple synchronous code to play CPU performance. It is a little bit of a package controller can be improved performance. Directly compatible with the old BIO architecture so far I’m using the default coroutine implementation which is the same as GO mindlessly launching virtual coroutines and turning on job stealing

Note:

The old BIO architecture refers to the blocking architecture based on the native Socket API in the JDK which is essentially still a blocking API but blocks Virtual threads instead of Platform threads.

The default scheduler uses a ForkJoin thread pool to enable work stealing

Transform SpringBoot

preface

For Spring reconstruction of the Boot on the Servlet container. Change the executor Executors newVirtualThreadExecutor ()

The embedded Tomcat is an example where the execution logic waits for the Controller to end without enabling asynchronous support

Even if you do this configuration, the internal Tomcat thread will block

@Configuration
public class TomcatConfiguration extends TomcatWebServerFactoryCustomizer {
​
​
    public TomcatConfiguration(Environment environment, ServerProperties serverProperties) {
        super(environment, serverProperties);
    }
​
    @Override
    public void customize(ConfigurableTomcatWebServerFactory factory) { factory.addProtocolHandlerCustomizers(protocolHandler -> protocolHandler.setExecutor(Executors.newVirtualThreadExecutor())); }}Copy the code

So you have to enable asynchronous servlet support to let the corresponding thread “leave” in time and write it out when the controller corresponding method completes

Check out the SpringMVC documentation

 @RequestMapping("/word")
    public DeferredResult<String> word(a) throws InterruptedException {
        final DeferredResult<String> result = new DeferredResult<>();
        Thread.startVirtualThread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result.setResult("string");
        });
        return result;
    }
Copy the code

Asynchronous responses are enabled with DeferredResult

In other words, we can change the interface to wrap it in DeferredResult and we can’t change the old code

They can only be replaced by proxies such as bytecode engineering

Because Java Agent requires a certain threshold and changes the original code significantly

So I used ByteBuddy to delegate forward +BeanPostProcessor to replace the injected Controller object at runtime

Why not enhance it by inheritance?

The AOP we use most often is based on bytecode inheritance of pre-existing classes through tools like CGLIB

But in this case, we cannot change the return value type due to inheritance overrides

So my idea is to make a delegate that looks something like this:

The following is the method that actually registers the route

public String wor1d(a) throws InterruptedException {
        System.out.println(Thread.currentThread());
        return "ADw";
    }
public DeferredResult<String> worldProxy(a){
     final DeferredResult<String> result = new DeferredResult<>();
        Thread.startVirtualThread(()->{
            result.setResult(world());
        });
        return result;
}
Copy the code

This way the stack can be blocked at will by synchronous writing in the virtual thread and is complete and continuous

Get the wrapped class

Pay attention to

Since I am not familiar with ByteBuddy, I only provide ideas and sample code

Package method generation

Due to code layout issues and personal level issues the code is for reference only

The actual idea is this

1, put all the annotations from the original class on the run-time produced class (@restController, etc.)

2. Wrap if not DeferredResult

3. Copy the method name

4. Perform these operations for each method

  1. Migrate all method annotations
  2. Copy the parameter annotations and also copy the parameter names

5. The method body uses a ControllerInterceptor (described below) to forward the actual call to the method that does the actual processing

Summary do a is in addition to the return value is not the same other methods are the same

public static DynamicType.Builder.MethodDefinition copyMethod(Class target, ControllerInterceptor interceptor){
        DynamicType.Builder.MethodDefinition now = null;
        DynamicType.Builder<Object> builder = new ByteBuddy()
                .subclass(Object.class)
                .annotateType(target.getAnnotations());
        final List<Method> methodList = Arrays.stream(target.getDeclaredMethods()).filter(m -> Modifier.isPublic(m.getModifiers())).collect(Collectors.toList());
        for (Method method : methodList) {
            TypeDescription.Generic returnType;
            if (method.getReturnType() == DeferredResult.class){
                returnType = TypeDefinition.Sort.describe(method.getGenericReturnType());
            }else {
               returnType = TypeDescription.Generic.Builder.parameterizedType(DeferredResult.class, method.getGenericReturnType()).build();
            }
            if (now == null) {
                now = copyParameters(builder.defineMethod(method.getName(), returnType, Modifier.PUBLIC), method)
                        .intercept(MethodDelegation.to(interceptor))
                        .annotateMethod(method.getAnnotations());
            }else{ now = copyParameters(now.defineMethod(method.getName(), returnType, Modifier.PUBLIC), method) .intercept(MethodDelegation.to(interceptor)) .annotateMethod(method.getAnnotations()); }}return now;
    }
Copy the code

The forwarding agent

@RuntimeType
    public Object interceptor(@Origin Method method, @AllArguments Object[] args) throws InvocationTargetException, IllegalAccessException {
        DeferredResult<Object> result = new DeferredResult<Object>();
        Thread.startVirtualThread(()->{
            try {
         // Notice that this method is actually the method of the class generated by my runtime
         //getTargetMethod is the method name + argument list to find the corresponding method in the cache that really needs to be called
                Object res = getTargetMethod(method).invoke(target, args);
                result.setResult(res);
            } catch(Throwable e) { result.setErrorResult(e); }});return result;
    }
Copy the code

Actual generated object

Originally method

Generated methods (image of bytecode decompilation result)

g.

public class LoomControllerRegister implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {if (isRestController(bean)){//wrapLoom replaces wrapLoom(bean) with a dynamically generated class; } return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); } / /... Space limit does not show}Copy the code

Finally add it to your entrance

@Import(LoomControllerRegister.class)
Copy the code

Just import this class

You don’t have to change the code and SpringBoot has its own mechanism for handling Deferredresults

Perfectly embedded and compatible with any other servlet container

See detailed code

Loomdemo: Non-invasive ByteBuddy Bytecode Generation adds Loom virtual threads to Spring Boot support for Servlet container independence (gitee.com)