Reducing Clojure Lambda Cold Starts Part 5 - Native JVM vs Node Performance

Search for a command to run...

No comments yet. Be the first to comment.
Things are looking really good for ClojureScript Lambdas over Clojure ones. From the results in my previous posts, the cold start times are way better and even just some basic code to list buckets from S3 ran significantly faster. In this post we wil...
If you are using the v2 AWS SDK clients, you can configure them like so: (ns my.s3 (:import (java.net URI) (software.amazon.awssdk.regions Region) (software.amazon.awssdk.services.s3 S3Client) (software.amazon.awssdk.services.s3.mo...
Last time we created our Rust and Typescript Lambdas with basic hello world implementations and did a quick performance comparison. We'll now expand our Rust and Typescript Lambdas from last time into ones that take data from SQS messages and push th...

In this series, I will be investigating throughput tuning for a Lambda that receives SQS events, reads data from S3 object, and blasts the data into DynamoDB. While I'm at it, I'll do a performance shootout between Rust and Typescript versions, attem...

Where Lambda cold starts often get worse in other runtimes is when you start adding dependencies, particularly an AWS SDK dependency. Let's see how Rust fares with an S3 client dependency. Updating Cargo.toml: [package] name = "tax_engine_experiments...

Rust seems to be at the height of the hype cycle right now even among functional programming enthusiasts. Although it's not a true functional programming language, due to not having first-class support for immutable data structures, its ownership mod...

On this page
Comparing the performance of ClojureScript vs Clojure Lambda got me wondering what the performance difference is between them and the comparable JavaScript and Java Lambda with the same dependencies and essentially the same code. I'll create such Lambdas and run my SQS blaster against them and compare the results to the Clojure/Script versions.
JavaScript Lambdas are super easy, I can just add the code inline with no need for any build tools:
...
RunJavaScriptCalculationsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub "${AWS::StackName}-run-calcs-queue-js"
VisibilityTimeout: 5400
RunCalculationsJS:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-run-calcs-js"
Handler: index.handler
Runtime: nodejs14.x
Timeout: 900
MemorySize: 128
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:ListAllMyBuckets
Resource: 'arn:aws:s3:::*'
Environment:
Variables:
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunJavaScriptCalculationsQueue.Arn
BatchSize: 1
InlineCode: |
const AWS = require('aws-sdk')
const s3 = new AWS.S3()
exports.handler = async function(event) {
return s3.listBuckets().promise();
}
Deploying, running the SQS blaster, and running this query in Log Insights:
stats avg(@initDuration), avg(@duration), count(@initDuration), count(@duration)
Gives the results:
avg(@initDuration),avg(@duration),count(@initDuration),count(@duration)
445.55,150.6677,16,637
As I had hoped, the durations are nearly identical to the ClojureScript version.
Add src/core/tax/core.java:
package tax;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.ListBucketsRequest;
public class core {
public Object calculationsHandler(Object event) {
S3Client s3 = S3Client.builder().build();
ListBucketsRequest req = ListBucketsRequest.builder().build();
return s3.listBuckets(req).toString();
}
}
Update build.clj:
(ns build
(:require [clojure.tools.build.api :as b]
[clojure.java.shell :refer [sh]]))
(def lib 'taxbit/tax-engine)
(def version "0.1.0")
(def clj-target "target/clj")
(def java-target "target/java")
(def clj-class-dir (format "%s/classes" clj-target))
(def java-class-dir (format "%s/classes" java-target))
(def basis (b/create-basis {:project "deps.edn"}))
(defn uber-file [target-dir]
(format "%s/%s-%s-standalone.jar" target-dir (name lib) version))
(def clj-uber-file (uber-file clj-target))
(def java-uber-file (uber-file java-target))
(defn clean [path]
(b/delete {:path path}))
(defn uber [_]
(clean clj-target)
(b/copy-dir {:src-dirs ["src/clj" "resources"]
:target-dir clj-class-dir})
(b/compile-clj {:basis basis
:src-dirs ["src/clj"]
:class-dir clj-class-dir})
(b/uber {:class-dir clj-class-dir
:uber-file clj-uber-file
:basis basis
:main 'tax.core}))
(defn uber-java [_]
(clean java-target)
(b/copy-dir {:src-dirs ["src/java" "resources"]
:target-dir java-class-dir})
(b/javac {:basis basis
:src-dirs ["src/java"]
:class-dir java-class-dir
:javac-opts ["-source" "11" "-target" "11"]})
(b/uber {:class-dir java-class-dir
:uber-file java-uber-file
:basis basis
:main 'tax.core}))
Update template.yml:
...
RunCalculationsCLJ:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-run-calcs-clj"
Handler: tax.core::calculationsHandler
Runtime: java11
CodeUri: target/clj/tax-engine-0.1.0-standalone.jar
Timeout: 900
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:ListAllMyBuckets
Resource: 'arn:aws:s3:::*'
Environment:
Variables:
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunClojureCalculationsQueue.Arn
BatchSize: 1
RunCalculationsJava:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-run-calcs-java"
Handler: tax.core::calculationsHandler
Runtime: java11
CodeUri: target/java/tax-engine-0.1.0-standalone.jar
Timeout: 900
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:ListAllMyBuckets
Resource: 'arn:aws:s3:::*'
Environment:
Variables:
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunJavaCalculationsQueue.Arn
BatchSize: 1
...
Running the SQS blaster on the CLJ and Java versions and running Log Insights queries gives:
| Language | avg(@initDuration) | avg(@duration) | count(@initDuration) | count(@duration) | avg(@maxMemoryUsed) |
| Clojure | 3011.4847 | 701.1622 | 38 | 1000 | 197288000 |
| Java | 428.4675 | 530.562 | 28 | 1104 | 170933876 |
Yikes, I was not expecting such a difference in cold start times. I have been operating under the faulty assumption that most of the load time is due to the JVM, not to the additional Clojure loading! I also was not expecting the cold start for the Java version to be less than our JavaScript version! The durations after cold start are along the lines of what I was expecting, a little more for Clojure than Java, but @duration is the total duration and also includes the @initDuration, so the actual average durations are probably a bit closer.
So, to wrap up, ClojureScript and JavaScript Lambdas with the same dependencies seem to have nearly identical cold start times and overall run times. Java cold starts seem pretty comparable to JavaScript ones, but the run times were actually significantly slower for listBuckets. Clojure cold starts turn the base JVM ones from good to terrible, but the overall runtimes aren't too much worse. Seems to be another win for ClojureScript.