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
- Migrate all method annotations
- 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)