AWS Lambda SnapStart - Part 3 Measuring Java 11 Lambda cold starts with Quarkus framework
Tags: java, aws, serverless, quarkus
Published: 2022-12-21 16:41:22

## Introduction
In the [first](https://dev.to/vkazulkin/measuring-java-11-lambda-cold-starts-with-snapstart-part-1-first-impressions-30a4) and [second](https://dev.to/aws-builders/measuring-java-11-lambda-cold-starts-with-snapstart-part-2-using-micronaut-framework-17j4) part of the series we talked about the SnapStart in general and made the first tests to compare the cold start of Lambda written in Plain Java with AWS SDK for Java version 2 and using Micronaut Framework with and without SnapStart enabled. We saw that enabling SnapStart led to a huge decrease in the cold start times in both cases. In this part of the series we are going to measure the performance of SnapStart using Quarkus as another popular Java framework.

## Quarkus Framework and its features
[Quarkus Framework](https://quarkus.io/) is Kubernetes Native Java stack (personally I don't understand this term :)) tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards. For us it's important that this framework is very well suited for writing Serverless applications. Like Micronaut, it gives us the possibility to create our application using the [launcher](https://code.quarkus.io/) or CLI, provides API Gateway integration, GraalVM (Native Image) integation, supports Maven und Gradle and much more. It also supports [MicroProfile](https://microprofile.io/) and [Funqy](https://quarkus.io/guides/funqy) for multi cloud solutions. It also processes annotations at compile time and doesn't use reflection, runtime byte code generation, runtime generated proxies and dynamic class loading.

## Writing AWS Lambda with Quarkus

We'll use the same application as in the previous parts of this series, but we'll rewrite it to use Quarkus Framework. The code of this sample application can be found [here](https://github.com/Vadym79/AWSLambdaJavaSnapStart/tree/main/quarkus). It provides AWS API Gateway and 2 Lambda functions: "CreateProduct" and "GetProductById". The products are stored in the Amazon DynamoDB. We'll use AWS Serverless Application Model (AWS SAM) for the infrastructure as a code.
 
Let's look how to implement it with Quarkus. In the AWS SAM Template [(template.yaml)](https://github.com/Vadym79/AWSLambdaJavaSnapStart/blob/main/quarkus/template.yaml) we point the AWS Lambda function handler to the generic QuarkusStreamHandler implementation:

```
Globals:
  Function:
    Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
```

which comes from the dependency 

```
       <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-amazon-lambda</artifactId>
        </dependency>
```

declared in [pom.xml](https://github.com/Vadym79/AWSLambdaJavaSnapStart/blob/main/quarkus/pom.xml). 

Quarkus supports Spring Boot Annotations like @RestController, @GetMapping, @PostMapping, @PathParam as well as AWS Java SDK and MicroProfile and Quarkus own annotations. In our example we'll use the latter.

This is how [GetProductById](https://github.com/Vadym79/AWSLambdaJavaSnapStart/blob/main/quarkus/src/main/java/software/amazonaws/example/product/handler/GetProductByIdHandler.java) Lambda function implementation looks like:

```
@Named("getProductById")
public class GetProductByIdHandler implements RequestHandler<APIGatewayProxyRequestEvent, Optional<Product>> {
	  private final ProductDao productDao = new DynamoProductDao();

	  @Override
	  public Optional<Product> handleRequest(APIGatewayProxyRequestEvent event, Context context) {
			String id = event.getPathParameters().get("id");
		    return productDao.getProduct(id);
}
```
We also have to declare the Product Bean with @RegisterForReflection and helper class Products with @ApplicationScoped annotation	

The mapping between Lambda function definition and the code is done through these 2 following steps:

1) Set the value of QUARKUS_LAMBDA_HANDLER environment variable in the AWS SAM Template [(template.yaml)](https://github.com/Vadym79/AWSLambdaJavaSnapStart/blob/main/quarkus/template.yaml)

```
  GetProductByIdFunction:
    Type: AWS::Serverless::Function
.....
      Environment:
        Variables:
          QUARKUS_LAMBDA_HANDLER: getProductById
```
2) The value of the QUARKUS_LAMBDA_HANDLER environment variable (in this case getProductById) should match the @Named annotation value in the Lambda function handler.

```
@Named("getProductById")
public class GetProductByIdHandler implements RequestHandler<APIGatewayProxyRequestEvent, Optional<Product>> {

....
```
[QuarkusStreamHandler](https://quarkus.pro/guides/amazon-lambda.html) wires everything together. With this it's possible to define multiple Lambda functions in the same SAM template.


## Measuring the cold starts

Let's give the GetProductById Lambda Function 1024 MB of memory and first measure its the cold start without enabling the SnapStart on it. The CloudWatch Logs Insights Query for the /aws/lambda/GetProductByIdWithQuarkus Log Group for it is

```
filter @type="REPORT" | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldStart| stats count(*) as count,
pct(duration, 50) as p50,
pct(duration, 90) as p90,
pct(duration, 99) as p99,
max(duration) as max by coldStart
```

Here are the results after experiencing 100 cold starts for the same Lambda version:

**p50 3164.19
p90 3262.57
p99 3823.99**

If we compare these metrics with [AWS Lambda with plain Java](https://dev.to/vkazulkin/measuring-java-11-lambda-cold-starts-with-snapstart-part-1-first-impressions-30a4) (and AWS SDK for Java version 2) and with [Micronaut](https://dev.to/aws-builders/measuring-java-11-lambda-cold-starts-with-snapstart-part-2-using-micronaut-framework-17j4) we'll notice that the cold starts using the Quarkus Framework are by far the lowest. I'll explore the reason for it in the separate article, but I can assume that it's because of the very perfomant HTTP client required for the Amazon DynamoDB communication.

Since the version 2.15.0. Quarkus [officially supports Lambda SnapStart ](https://quarkus.io/blog/quarkus-2-15-0-final-released/). This means that now we can implement [CRaC (Coordinated Restore at Checkpoint) API](https://www.azul.com/blog/superfast-application-startup-java-on-crac/) in the Lambda functions written in Quarkus to further decrease the cold starts (this will be the topic of the separate article).
Let's enable SnapStart GetProductById Lambda Function like this

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xq9ygu6xlc3sio7d9xc.png) or directly in the [SAM template](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-snapstart.html).

It's also important to define **AutoPublishAlias** on this function in the SAM template
```
  GetProductByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: GetProductByIdWithQuarkus
      AutoPublishAlias: liveVersion
...
```
as we can use SnapStart only [on published function versions and aliases that point to versions](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html#snapstart-runtimes). In this case alias always points to the latest version by default.  The CloudWatch Logs Insights Query for the /aws/lambda/GetProductByIdWithQuarkus Log Group for it is

```
filter @type = "REPORT"
  | parse @message /Restore Duration: (?<restoreDuration>.*?) ms/
  | stats
count(*) as invocations,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 50) as p50,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 90) as p90,
pct(@duration+coalesce(@initDuration,0)+coalesce(restoreDuration,0), 99) as p99
group by function, (ispresent(@initDuration) or ispresent(restoreDuration)) as coldstart
  | sort by coldstart desc
```

Here are the results after experiencing 100 cold starts for the same Lambda version:

**p50 1337.16
p90 1374.76
p99 1473.87**

If we compare these metrics with AWS Lambda with plain Java (and AWS SDK for Java version 2) and Micronaut Framework with SnapStart enabled, we'll notice that using the Micronaut Framework the average cold start with Quarkus is quite comparable with the first and better than the latter. If we compile our application with GraalVM Native Image and deploy our Lambda as Custom Runtime (which is beyond the scope if this article), we can further reduce the cold start to between 450 and 550ms, see the [measurements](https://github.com/aws-samples/serverless-java-frameworks-samples) for the comparable application.

## Conclusions and next steps
In this blog post we looked into the Quarkus Framework and learned how to write AWS Lambda function. We also measured the cold start with and without enabling SnapStart for our scenario: Lambda receives the event from the Amazon API Gateway and reads the item from the Amazon DynamoDB. Quarkus showed the lowest cold starts comparing to the plain AWS SDK Version 2 Lambda function and Micronaut framework without SnapStart. With SnapStart enabled the cold start with Quarkus was better than with Micronaut and only slightly behind the plain AWS SDK Version 2 Lambda function. I personally can recommend using Quarkus. The well known programming model is a huge productivity booster.  I'll write a separate article about the memory consumption of the different frameworks and look deeper into how the choice of the different HTTP Clients impacts the cold start times.

In the next part of series we'll explore [Spring Boot](https://spring.io/projects/spring-boot) for writing Lambda functions and measure the cold start with and without enabling of the SnapSart.


**Update:** you can significantly reduce the cold start times of the Lambda function with SnapStart enabled further by applying the optimization technique called **priming**. Learn more about it in my [article](https://dev.to/aws-builders/measuring-java-11-lambda-cold-starts-with-snapstart-part-5-priming-end-to-end-latency-and-deployment-time-jem).