Will my lib or framework work on

(and GraalVM) ?

 

Peter Palaga

@ppalaga

Peter Palaga

Agenda

  • Quarkus intro
  • AoT1 compilation: goodies and gotchas
  • How to write Quarkus extensions

  • 1) Ahead of (run-)time

What is Quarkus

Toolkit

and

Framework

for writing Java, Kotlin and Scala applications

Embracing existing standards

Java EE MicroProfile Spring
  • Servlet
  • JAX-RS
  • JPA, JDBC
  • CDI
  • Bean Validation
  • Transactions
  • Fault Tolerance
  • Health
  • JWT
  • Metrics
  • OpenAPI
  • OpenTracing
  • Reactive Messaging
  • Rest Client
  • Spring DI1
  • Spring Web1
  • Spring Data1
  • Spring Security1

1) via compatibility layers
Well established

Libs and frameworks

Eclipse Vert.x Netty Apache Camel Infinispan Caffeine Keycloak
Kubernetes AWS Lambda Azure Functions Apache Tika ElasticSearch Kogito

Databases


  • MySQL, MariaDB
  • PostgreSQL
  • MS SQL Server
  • H2
  • FlyWay
  • Amazon DynamoDB‎
  • MongoDB
  • Neo4j

Developer joy

  • Easy to start with:
    mvn quarkus:create or code.quarkus.io
  • Live reload:
    mvn compile quarkus:dev
  • Runners for JUnit 5
  • Isolation from GraalVM CLI
  • Maven or Gradle
  • Java, Kotlin or Scala

Container First

💾 Small size on disk Small container images
🚀 Fast boot time Instant scale up
🔬 Low memory footprint More containers with the same RAM

AoT1 compilation

 

its goodies and gotchas


1) Ahead of (run-)time

AoT compilation with GraalVM

 

the good parts
$ native-image -jar my-app.jar
$ ls -lh
-rwxrwxr-x. 1 ppalaga ppalaga 19M Mar 20 14:39 my-app
$ ./my-app
...
my-app started in <20 ms
$ ps -o rss,command -p $(pgrep my-app)
  PID   RSS COMMAND
11229 12628 ./my-app
#      ⮤ the process memory in kilobytes

The

gotchas

of the AoT compilation

An incomplete list

The

closed world

assumption

 

All runtime code has to be known at build time

 

  • More effective static analysis
  • Dead code elimination:
        classes, fields, methods, branches
unsupported

Dynaminc Classloading

in a native executable



Deloying jars, wars, etc. at runtime impossible

registration required

Reflection

Dynamic proxies

Classpath resources

JNI, Unsafe Memory Access

...

Typically invoked only at compile time

Class initializers

1/2

At build time:

  • Resolve classes, run "safe" static initilizers
  • Take a snapshot of the produced heap
  • Store it in the executable
Typically invoked only at compile time

Class initializers

2/2

Downsides:

no file handles, sockets, threads

Complex CLI

$ native-image -jar my-app.jar \
    -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
    -J-Djava.util.concurrent.ForkJoinPool.common.parallelism=1 \
    -H:FallbackThreshold=0 \
    -H:ReflectionConfigurationFiles=file-with-tens-of-entries.json
    -H:+ReportExceptionStackTraces \
    -H:+PrintAnalysisCallTree \
    -H:-AddAllCharsets \
    -H:EnableURLProtocols=http \
    -H:-JNI \
    -H:-UseServiceLoaderFeature \
    -H:+StackTrace \
    --no-server \
    --initialize-at-build-time=my-build-time-init-list.json \
    -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
    -J-Dio.netty.leakDetection.level=DISABLED \
    -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
    -J-Dsun.nio.ch.maxUpdateArraySize=100 \
    -J-Dio.netty.allocator.maxOrder=1 \
    -J-Dvertx.disableDnsResolver=true

How Quarkus works

Booting a framework

  • Many classes run only during the boot
  • Later unused
  • Still occupy memory
  • Parse config files: XML, YAML, JSON, ...
  • Classpath scanning, esp. for annotations
  • Build framework metamodel objects
  • Prepare reflection and build proxies
  • Open sockets, start threads
Quarkus:

Build time boot

As much work as possible done at build time

Output: recorded bootstrap bytecode

Build oriented container

Quarkus extensions


  • Units of Quarkus distribution (your Maven deps)
  • Configure, boot and integrate a lib/framework into a Quarkus application
  • Make the code lighter to run on a JVM
  • Make the code fit for the GraalVM

Quarkus Extensions ecosystem


How to write

Quarkus Extensions

Two Maven modules

depends on
Build time module
my-ext-deployment
 
class MyExtProcessor {
  @BuildStep
  ... buildStep1(...) {...}

  @BuildStep
  ... buildStep2(...) {...}

  ...
}
Runtime module
my-ext
@Recorder
class MyExtRecorder {

  ... runtimeTask1(...) {...}


  ... runtimeTask2(...) {...}

  ...
}
class MyExtProcessor {

  /* BuildSteps typically produce BuildItems */
  @BuildStep
  NameBuildItem nameStep() {
    return new NameBuildItem("Joe");
  }

  /* BuildItems carry some payload */
  final class NameBuildItem extends SimpleBuildItem {
    final String name; /* The payload */
    ...
  }
}
class MyExtProcessor {

  /* BuildSteps may consume BuildItems */
  @BuildStep
  HelloBuildItem helloStep(NameBuildItem nameItem) {
    return new HelloBuildItem("Hello " + nameItem.getName());
  }

  @BuildStep
  NameBuildItem nameStep() {
    return new NameBuildItem("Joe");
  }
  /* helloStep depending on the nameStep
   * determines the execution order */
}
class MyExtProcessor {

  @BuildStep
  void helloStep(
        NameBuildItem nameItem,          /* BuildSteps may consume*/
        GreetingBuildItem greetingItem,  /* multiple BuildItems */
        /* Results can also be published via BuildProducers */
        BuildProducer<HelloBuildItem> helloProducer,
        BuildProducer<FooBuildItem> fooProducer) {

    /* BuildSteps may produce multiple BuildItems */
    helloProducer.produce(
        new HelloBuildItem(
            greetingItem.getGreeting() + " " + nameItem.getName());

    fooProducer.produce(new FooBuildItem());
  }
}

Jandex

A Java annotation indexer
and offline reflection library.


Used by @BuildSteps
to inspect the application code


https://github.com/wildfly/jandex

A common @BuildStep:
 

  1. Find classes having some annotation
     
  2. Register them for reflection
class JacksonProcessor {
  @BuildStep
  void registerDeserializers(
        CombinedIndexBuildItem combinedIndex, /* Pass the Jandex */
        BuildProducer<ReflectiveHierarchyBuildItem> reflProducer) {

    DotName JSON_DESERIALIZE =
        DotName.createSimple(JsonDeserialize.class.getName());
    for (AnnotationInstance annot    /* Query for the annotations */
        : combinedIndex.getIndex().getAnnotations(JSON_DESERIALIZE)) {
      /* Do some filtering */
      AnnotationTarget annotTarget = annot.target();
      if (CLASS.equals(annotTarget.kind())) {
        DotName dotName = annotTarget.asClass().name();
        Type jandexType = Type.create(dotName, Type.Kind.CLASS);
        /* ... and finally register the type for reflextion */
        reflProducer.produce(new ReflectiveHierarchyBuildItem(jandexType));
}}}}
Code Adapted from the Jackson Extension

More Jandex usecases

  • Find classes/fields/methods having some annotation
  • Find classes implementing an interface
  • Find subclasses of a class
  • List fields, methods and constructors of a class
Build time module
my-ext-deployment
 
class MyExtProcessor {
  @BuildStep
  ... buildStep1(...) {...}

  @BuildStep
  ... buildStep2(...) {...}

  ...
}
Runtime module
my-ext
@Recorder
class MyExtRecorder {

  ... runtimeTask1(...) {...}


  ... runtimeTask2(...) {...}

  ...
}
chunks of application bootstrap code
class CamelProcessor {
  @Record(ExecutionTime.STATIC_INIT)  /*    @Recorders can be injected  */
  @BuildStep                          /* ⮦ as BuildStep method params  */
  CamelContextBuildItem context(CamelRecorder recorder) {
    RuntimeValue<CamelContext> context = recorder.createContext();
    return new CamelContextBuildItem(context);  /* ⮤ @BuildStep methods */
  }/* ⮤ The RuntimeValue can be dispatched */   /*   can invoke        */
}  /*  to other @BuildSteps via a BuildItem */   /*   @Recorder methods */

@Recorder
public class CamelRecorder {
  public RuntimeValue<CamelContext> createContext() {
           /* ⮤ A handle to pass values between recorders */
    FastCamelContext context = new FastCamelContext();
    return new RuntimeValue<>(context);
}}

Code Adapted from the Apache Camel Quarkus

Extensions may depend on each other

E.g. mp-metrics extension
depends on
camel-quarkus-core

so it can consume the CamelContextBuildItem and adjust the CamelContext as needed

class MpMetricsProcessor {
  @Record(ExecutionTime.STATIC_INIT)
  @BuildStep
  public void configureCamelContext(
        MpMetricsRecorder recorder,
        CamelContextBuildItem ctxItem) {
    recorder.configureCamelContext(ctxItem.getCamelContext());
}}

@Recorder
public class MpMetricsRecorder {
  public void configureCamelContext(RuntimeValue<CamelContext> ctxVal) {
               /* Unwrap the RuntimeValue ⮧ */
    CamelContext camelContext = ctxVal.getValue();
    /* ... and configure it as needed */
    ManagementStrategy strategy = camelContext.getManagementStrategy();
    strategy.addEventNotifier(new MpMetricsCamelContextEventNotifier());
}}
Code Adapted from the Apache Camel Quarkus

Resulting application bootstrap code:

class GeneratedMain { /* bytecode in reality, pseudocode here */
  static { /* Chunks recorded with @Record(ExecutionTime.STATIC_INIT) */
    CamelRecorder camelRecorder = new CamelRecorder();
    RuntimeValue<CamelContext> val1 = camelRecorder.createContext();
    /* The order of the calls is given by
     * the dependencies between the @BuildSteps */
    MpMetricsRecorder mpMetricsRecorder = new MpMetricsRecorder();
    mpMetricsRecorder.configureCamelContext(val1);
  }

  public static main(String[] args) {
    /* Chunks recorded with @Record(ExecutionTime.RUNTIME_INIT) */
    new VertxRecorder().openSocket(); /* can't do this in static init */
}}

Mostly used BuildItems

  • ReflectiveClassBuildItem, ReflectiveHierarchyBuildItem
       register classes for reflection in the native mode
  • AdditionalBeanBuildItem, BeanDefiningAnnotationBuildItem
       bean classes the CDI container should analyze
  • ExtensionSslNativeSupportBuildItem
       turn on SSL in the native mode
  • CombinedIndexBuildItem, ApplicationIndexBuildItem
       offline reflection and annotation index (Jandex)
  • NativeImageResourceBuildItem
       src/main/resources and resources from JARs not included by default in the native image

Extension configuration

  • Extensions may expose their configuration through annotated POJOs
  • These may come in three flavors:
    • BUILD_TIME
    • BUILD_AND_RUN_TIME_FIXED
    • RUN_TIME

Config POJO

@ConfigRoot(name = "camel", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class CamelConfig {

    @ConfigItem
    public MainConfig main;

    @ConfigGroup
    public static class MainConfig {
        @ConfigItem(defaultValue = "true")
        public boolean enabled;
        /* This option can be set in application.properties via
         *
         *   camel.main.enabled = true
         *
         */
    }
}
                        
Code Adapted from the Apache Camel Quarkus

Config in @BuildSteps

class CamelProcessor {
  @BuildStep
  ... discoverRoutesBuilderClassNames(CamelConfig config, ...) {
    if (config.main.enabled) {
        /* Do something */
    }
  }
}
                        
Code Adapted from the Apache Camel Quarkus

Config in @Recorders

@Recorder
public class MyRecorder {
  void record(MyRuntimeConfig config) {

    if (config.my.option) {
      /* Do something */
    }

    /* Same effect using
     * org.eclipse.microprofile.config.ConfigProvider */
    if (ConfigProvider.getConfig()
        .getValue("my-root.my.option", String.class)) {
      /* Do something */
    }
}}
Code Adapted from the Apache Camel Quarkus

Substitutions

  • Provided by GraalVM
  • For replacing classes in the native image

Substitutions usecases

  • Last resort for fixing third party code
    • E.g. avoid opening a socket, starting a Thread, etc. in a static initializer
  • Cut off uneeded code (and its dependencies)
    • E.g. parsing an XML config that is not supported on Quarkus

A substitution example

(1/2)
/* The original class (simplified) */
package com.mysql.cj.jdbc;
public class AbandonedConnectionCleanupThread implements Runnable {
  private static final ExecutorService cleanupThreadExcecutorService;
  private static Thread threadRef = null;

  static {
    cleanupThreadExcecutorService =
        Executors.newSingleThreadExecutor(r -> { ... });
    /* GraalVM does not like threads being started in a static block */
    cleanupThreadExcecutorService.execute(
        new AbandonedConnectionCleanupThread());
  }
  ...
}
Code Adapted from the MySQL JDBC driver

A substitution example

(2/2)
@Substitute /* The substitution class */
@TargetClass(AbandonedConnectionCleanupThread.class)
final public class AbandonedConnectionCleanupThread_disable {

  /* No static initializer here */

  @Substitute
  protected static void trackConnection(
      MysqlConnection conn, NetworkResources io) {
      /* A no-op method */
  }
}
Code Adapted from the MySQL JDBC extension

Demo


https://github.com/ppalaga/quarkus-ahc

Writing extensions

Wrap up

  1. Shrink and speedup the app
    • Inspect the app code (annotations, interfaces, config files ...) at build time
    • Bootstrap and configure the app via @Recorder methods
  2. Make the AoT compiler happy
    • Register reflection, proxies, JNI, ...
    • Resources to include in the native image
    • Substitutions

Will my lib work?

Will my framework work?