mvysny/vaadin-boot

Improve start speed via QuickStartWebApp

Closed this issue · 8 comments

mvysny commented

Jetty's QuickStartWebApp mechanism allow to generate a Jetty config file in build time, which would then remove the need to waste time in classpath scanning in runtime, allowing for far faster startup times.

Couple of links:

mvysny commented

The org.eclipse.jetty:jetty-quickstart Maven artifact contains related classes, both for Jetty 10 and 11. Documentation:

mvysny commented

Sadly mvn -C jetty:effective-web-xml fails with Packaging type [jar] is unsupported.

mvysny commented

I've filed a feature request for the Maven plugin at jetty/jetty.project#9497 . Meanwhile, perhaps I could experiment with the QuickStartGeneratorConfiguration.generateQuickStartWebXml() function in Gradle, perhaps take a look at Maven plugin sources: https://github.com/eclipse/jetty.project/tree/jetty-11.0.x/jetty-maven-plugin

mvysny commented

I've invoked the generator myself; the speed improvement is not that ground-breaking: 822ms vs 1200ms startup time. Perhaps that's because the classpath scanning runs in parallel on all cores: the performance improvement could be much bigger on a single-core machines (or single-core docker images).

The code which generated the file:

    private static final File quickstartXml = new File("src/main/resources/webapp/WEB-INF/quickstart-web.xml");
            context.setAttribute(ExtraXmlDescriptorProcessor.class.getName(), new ExtraXmlDescriptorProcessor());
            try (OutputStream out = new BufferedOutputStream(new FileOutputStream(quickstartXml))) {
                new QuickStartGeneratorConfiguration().generateQuickStartWebXml(context, out);
            }

Jetty server needs to be started to perform the classpath detection. To configure WebAppContext to reuse the generated quickstart-web.xml, configure the WebAppContext as follows:

            context.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.QUICKSTART);
            context.addConfiguration(new QuickStartConfiguration());
mvysny commented

The best solution would be if the quickstart-web.xml file could be generated at compile-time, by performing classpath scanning on all dependency jars and in-project files. That is unfortunately not possible at the moment, blocked by jetty/jetty.project#9497 .

There's no one-fits-all xml unfortunately since it lists all Vaadin Routes for example, which means that the xml needs to be updated whenever there's a new route; also the xml differs for dev mode and production mode (since production mode doesn't run DevModeStartupListener).

That being said, I can still add some basic support for quickstart since it will be valuable for native builds.

The src/main/resources/webapp/WEB-INF/quickstart-web.xml file for reference follows:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" metadata-complete="false" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="5.0">
  <context-param>
    <param-name>org.eclipse.jetty.containerInitializers</param-name>
    <param-value><![CDATA[
    "ContainerInitializer{com.vaadin.flow.server.startup.WebComponentExporterAwareValidator,interested=[],applicable=[],annotated=[]}",
    "ContainerInitializer{org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer,interested=[org.atmosphere.container.JSR356Endpoint],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.RouteRegistryInitializer,interested=[com.example.MainView],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.WebComponentConfigurationRegistryInitializer,interested=[com.vaadin.flow.component.WebComponentExporterFactory$DefaultWebComponentExporterFactory],applicable=[],annotated=[]}",
    "ContainerInitializer{org.eclipse.jetty.websocket.jakarta.client.JakartaWebSocketShutdownContainer,interested=[],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.VaadinAppShellInitializer,interested=[com.example.AppShell],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.LookupServletContainerInitializer,interested=[com.vaadin.flow.di.LookupInitializer$AppShellPredicateImpl, com.vaadin.flow.router.DefaultRoutePathProvider, com.vaadin.flow.di.LookupInitializer$StaticFileHandlerFactoryImpl, com.vaadin.flow.di.LookupInitializer, com.vaadin.flow.di.LookupInitializer$ResourceProviderImpl, com.vaadin.flow.server.startup.DefaultApplicationConfigurationFactory],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.AnnotationValidator,interested=[],applicable=[],annotated=[]}",
    "ContainerInitializer{org.atmosphere.cpr.ContainerInitializer,interested=[],applicable=[],annotated=[]}",
    "ContainerInitializer{com.vaadin.flow.server.startup.ErrorNavigationTargetInitializer,interested=[com.vaadin.flow.router.RouteNotFoundError, com.vaadin.flow.router.InternalServerError],applicable=[],annotated=[]}",
    "ContainerInitializer{org.atmosphere.cpr.AnnotationScanningServletContainerInitializer,interested=[org.atmosphere.annotation.BroadcasterCacheServiceProcessor, org.atmosphere.annotation.AtmosphereHandlerServiceProcessor, org.atmosphere.annotation.BroadcasterListenerServiceProcessor, org.atmosphere.annotation.AtmosphereServiceProcessor, org.atmosphere.annotation.BroadcasterCacheListenererviceProcessor, org.atmosphere.annotation.AtmosphereFrameworkServiceProcessor, org.atmosphere.annotation.AsyncSupportListenerServiceProcessor, org.atmosphere.annotation.WebSocketFactoryServiceProcessor, org.atmosphere.annotation.BroadcasterServiceProcessor, org.atmosphere.annotation.EndpointMapperServiceProcessor, org.atmosphere.annotation.BroadcasterFactoryServiceProcessor, org.atmosphere.annotation.ManagedServiceProcessor, org.atmosphere.annotation.UUIDProviderServiceProcessor, org.atmosphere.annotation.AtmosphereResourceListenerServiceProcessor, org.atmosphere.annotation.BroadcasterCacheInspectorServiceProcessor, org.atmosphere.annotation.AtmosphereInterceptorServiceProcessor, org.atmosphere.annotation.MeteorServiceProcessor, org.atmosphere.annotation.WebSocketProtocolServiceProcessor, org.atmosphere.annotation.BroadcastFilterServiceProcessor, org.atmosphere.annotation.WebSocketProcessorServiceProcessor, org.atmosphere.annotation.AsyncSupportServiceProcessor, org.atmosphere.annotation.WebSocketHandlerServiceProcessor, org.atmosphere.annotation.AtmosphereResourceFactoryServiceProcessor],applicable=[],annotated=[]}"]]></param-value>
  </context-param>
  <context-param>
    <param-name>org.eclipse.jetty.originAttribute</param-name>
    <param-value>origin</param-value>
  </context-param>
  <listener origin="DefaultsDescriptor(jar:file:/home/mavi/.gradle/caches/modules-2/files-2.1/org.eclipse.jetty/jetty-webapp/11.0.14/15e01322cc54172e474357f8ae8dd4ec15e5ed09/jetty-webapp-11.0.14.jar!/org/eclipse/jetty/webapp/webdefault.xml):0">
    <listener-class>org.eclipse.jetty.servlet.listener.IntrospectorCleaner</listener-class>
  </listener>
  <listener origin="@WebListener(com.example.Bootstrap):1">
    <listener-class>com.example.Bootstrap</listener-class>
  </listener>
  <listener origin="@WebListener(com.vaadin.flow.server.startup.ServletContextListeners):2">
    <listener-class>com.vaadin.flow.server.startup.ServletContextListeners</listener-class>
  </listener>
  <listener origin="@WebListener(com.vaadin.flow.server.startup.VaadinAppShellInitializer):3">
    <listener-class>com.vaadin.flow.server.startup.VaadinAppShellInitializer</listener-class>
  </listener>
  <listener>
    <listener-class>org.eclipse.jetty.websocket.jakarta.client.JakartaWebSocketShutdownContainer</listener-class>
  </listener>
</web-app>

I'll add both:

  1. A way to generate this xml file. At the moment the file will be generated by a call to a special method at runtime, it's not perfect but will work until jetty/jetty.project#9497 is fixed. Alternatively we can generate the XML file ourselves, by performing a classpath scan ourselves.
  2. A support to activate the file if it's on the classpath. Note that the file differs for dev mode and production mode, therefore we should expect the file to target production mode and ignore the file for dev mode by default. I propose a three-state QuickStart enum: Off, Production, Always. Off would be the default and would work as currently - always doing classpath scanning. Production would only activate the QuickStart during production mode and would fail if the quickstart xml file is missing.
mvysny commented

Showstopper: if I add the org.eclipse.jetty:jetty-quickstart artifact on classpath by default, the QuickStartConfiguration is enabled automatically by default, and then interferes with Jetty. If ctx.baseResource = EmptyResource.INSTANCE is used, then QuickStartConfiguration fails at 95 with IllegalStateException("Bad Quickstart location"). ctx.removeConfiguration(QuickStartConfiguration::class.java) doesn't help.

mvysny commented

Need to revert and remove the jetty-quickstart. Instead I'll document the steps necessary.

mvysny commented

Reopening: the new Jetty Maven plugin needs to be tested; README would benefit from the plugin.