AWS SnapStart - Part 14 Measuring cold and warm starts with Java 21 using different compilation options
Tags: aws, java, serverless, coldstart
Published: 2024-01-29 16:03:08

## Introduction

In the previous parts we've done many measurements with AWS Lambda using Java 21 runtime with and without using AWS SnapStart and additionally using SnapStart and priming DynamoDB invocation :

-  cold starts using [different deployment artifact sizes](https://dev.to/aws-builders/aws-snapstart-part-11-measuring-cold-starts-with-java-21-using-different-deployment-artifact-sizes-4g29)
- cold starts and deployment time using [different Lambda memory settings ](https://dev.to/aws-builders/aws-snapstart-part-12-measuring-cold-starts-and-deployment-time-with-java-21-using-different-lambda-memory-settings-35hl)
- warm starts [using different Lambda memory settings ](https://dev.to/aws-builders/aws-snapstart-part-13-measuring-warm-starts-with-java-21-using-different-lambda-memory-settings-160k)


We've done all those measurements using the following JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" defined in the AWS SAM template.yaml. This means that client compilation (c1) without profiling will be applied. It was considered the best choice due to the article [Optimizing AWS Lambda function performance for Java](https://aws.amazon.com/de/blogs/compute/optimizing-aws-lambda-function-performance-for-java/) by Mark Sailes. But all these measurements have been done for Java 11 and before Lambda SnapStart has been released. So now it's time to revisit this topic and measure cold and warm start times with different Java compilation options without SnapStart enabled, with SnapStart enabled (and additionally with priming). In this article we'll do it for Java 21 runtime and later in the subsequent parts with Java 17.

## Meaning of Java compilation options

This picture shows Java compilation available.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6bh253so1ok36kchubb.png)

If you don't specify any options, the default one applied for will be tiered compilation. You can read more about it in this article [Tiered Compilation in JVM](https://www.baeldung.com/jvm-tiered-compilation) or generally about client (C1) and server (C2) compilation in the article [Client, Server, and Tiered Compilation](https://dzone.com/articles/client-server-and-tiered-compilation). There are also many other settings so you can apply to each of the compilation options. You can read more about them in this article [JVM c1, c2 compiler thread – high CPU consumption?](https://blog.fastthread.io/2022/05/26/jvm-c1-c2-compiler-thread-high-cpu-consumption/)


## Measuring cold starts and deployment time with Java 21 using different compilation options

For measurement purposes I created the sample application and configured Lambda functions to use Java 21 runtime for Lambda and 1024 MB memory which we introduced in [part 9](https://dev.to/aws-builders/measuring-lambda-cold-starts-with-aws-snapstart-part-9-measuring-with-java-21-5e52). Here is the code for our [sample application](https://github.com/Vadym79/AWSLambdaJavaSnapStart/tree/main/pure-lambda-21)

There are basically 2 Lambda functions which both respond to the API Gateway requests and retrieve product by id received from the API Gateway from DynamoDB. One Lambda function GetProductByIdWithPureJava21Lambda can be used with and without SnapStart and the second one GetProductByIdWithPureJava21LambdaAndPriming uses SnapStart and DynamoDB request invocation priming.

The results of the experiment below were based on reproducing more than 100 cold and approximately 100.000 warm starts. For it (and experiments from my previous article) I used the load test tool [hey](https://github.com/rakyll/hey), but you can use whatever tool you want, like [Serverless-artillery](https://www.npmjs.com/package/serverless-artillery) or [Postman](https://www.postman.com/). I ran all these experiments with 5 different compilation options defined in the [template.yaml](https://github.com/Vadym79/AWSLambdaJavaSnapStart/blob/main/pure-lambda-21/template.yaml). This happens in the Globals section where variable named "JAVA_TOOL_OPTIONS" is defined in the Environment section of the Lambda function:

```
Globals:
  Function:
    CodeUri: target/aws-pure-lambda-snap-start-21-1.0.0-SNAPSHOT.jar
    Runtime: java21
    ....
    Environment:
      Variables:
        JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
```

1. no options (tiered compilation will take place)
2. JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"  (client/C1 compilation without profiling)
3. JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=2"  (client/C1 compilation with basic profiling)
4. JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=3"  (client/C1 compilation with full profiling)
5. JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=4" (server/C2 compilation)


For their meaning see our explanations above. We will refer to those compilation options in the table column "Compilation Option" by their number in the tables below, for example number 5 stays for JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=4". Abbreviation **c** is for the cold start and **w** is for the warm start.

**Cold (c) and warm (w) start times without SnapStart in ms:**

|Compilation Option| c p50  | c p75 | c p90 |c p99 | c p99.9| c max |w p50 | w p75 | w p90 |w p99 | w p99.9 | w max |      
|-----------|----------|-----------|----------|----------|----------|----------|-----------|----------|----------|----------|----------|----------|
|1|3175.4|3299.88|3524.78|4056.3|4192.12|4365.72|6.01|6.93|8.52|  22.09|101.41|1479.68|
|2|3157.6|3213.85|3270.8|3428.2|3601.12|3725.02|5.77|6.50|7.81|  20.65|90.20|1423.63|
|3|3322.76|3430.37|3498.45|3781.3|3901.11|4041.01|6.21|7.16|8.94| 23.921|326.94|1518.75|
|4|3625.35|3730.16|3838.98|3906.98|4043.12|4172.62|6.71|7.75|9.23|25.49|98.24|1636.91|
|5|4864.52|4984.32|5096.54|5572.38|5732.78|5895.93|6.41|7.63|11.16|28.48|2036.95|2586.29|

**Cold (c) and warm (w) start times with SnapStart without Priming in ms:**

|Compilation Option| c p50  | c p75 | c p90 |c p99 | c p99.9| c max |w p50 | w p75 | w p90 |w p99 | w p99.9 | w max |      
|-----------|----------|-----------|----------|----------|----------|----------|-----------|----------|----------|----------|----------|----------|
|1|1649.61|1691.35|1772.70|1920.27|1976.74|1978.35|5.96|6.77|8.20|22.01|100.04|1234.03|
|2|1626.69|1741.10|2040.99|2219.75|2319.54|2321.64|5.64|6.41|7.87|21.40|99.81|1355.09|
|3|1734.15|1804.88|2115.77|2394.93|2438.41|2440.14|6.01|6.83|8.38|23.17|111.54|1460.25|
|4|1874.75|1966.89|2411.75|2738.16|2807.45|2810.23|6.72|7.63|9.08|25.49|95.17|1632.01|
|5|2713.64|2852.70|3139.99|3377.66|3384.42|3386.76|6.30|7.51|10.65|29.40|98.23|2346.81|

**Cold (c) and warm (w) start times with SnapStart and with DynamoDB invocation Priming in ms:**

|Compilation Option| c p50  | c p75 | c p90 |c p99 | c p99.9| c max |w p50 | w p75 | w p90 |w p99 | w p99.9 | w max |      
|-----------|----------|-----------|----------|----------|----------|----------|-----------|----------|----------|----------|----------|----------|
|1|686.59|734.88|821.92|921.12|938.78|938.79|5.73|6.61|8.00|21.74|130.73|260.48|
|2|702.55|759.52|1038.50|1169.66|1179.05|1179.36|5.73|6.51|7.87|  21.75|92.19|328.41|
|3|785.77|887.68|1042.66|1235.76|1264.50|1264.81|6.01|6.93|8.66| 23.17|146.09|273.32|
|4|813.75|903.79|1260.71|1364.29|1368.39|1369.64|6.83|7.87|9.53| 26.73|113.32|313.03|
|5|828.52|971.23|1198.06|1417.11|1649.61|1649.82|6.21|7.39|10.15| 28.03|98.24|406.48|

## Conclusions

For all measurements we discovered that setting compilation options -XX:+TieredCompilation -XX:TieredStopAtLevel= 2, 3 or 4 produced much worse cold and warm starts as the tiered compilation or -XX:TieredStopAtLevel=1 (client compilation without profling).

For the Lambda function without SnapStart enabled client compilation without profiling (-XX:+TieredCompilation -XX:TieredStopAtLevel=1 ) is the better option for having lower cold and warm starts for nearly all percentiles for our use case.

For the Lambda function with SnapStart enabled (with priming of DynamoDB invocation or without priming) tiered compilation (the default one) outperformed the client compilation without profiling (-XX:+TieredCompilation -XX:TieredStopAtLevel=1 ) in terms of the lower cold start time and also for the warm start time for nearly all percentiles for our use case.  

So please review/re-measure the cold and warm start times for your use case if you use Java 21, as tiered compilation can be a better choice if you enable SnapStart for your Lambda function(s). In our case we didn't use any framework like Spring Boot, Micronaut or Quarkus which may also impact the measurements.

In one of my next articles I will make the same measurements but using Java 17 runtime.