Reducing Clojure Lambda Cold Starts Part 4 - JVM vs Node Performance

Search for a command to run...

No comments yet. Be the first to comment.
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 Lam...
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
ClojureScript Lambdas on Node seem promising so far, with an average cold start time of 182.5228 ms vs. 2.6567039 seconds for a similarly bare-bones Clojure Lambda on the JVM. But how will they compare when it comes to performing more realistic workloads?
I'll configure the SDKs and then use listBuckets to test the configuration.
In src/cljs/tax/core.cljs:
(ns tax.core
(:require [cljs.core.async :as async :refer [<!]]
[cljs.core.async.interop :refer-macros [<p!]]
["aws-sdk" :as aws])
(:require-macros [cljs.core.async.macros :refer [go]]))
(def client (aws/S3.))
(defn list-buckets []
(.promise (.listBuckets client)))
(defn handler [event context callback]
(go (let [result (<p! (list-buckets))]
(callback nil result))))
In deps.edn:
{:paths ["src/clj"]
:deps {software.amazon.awssdk/s3 {:mvn/version "2.17.100"}}
:aliases {:build {:deps {io.github.clojure/tools.build {:tag "v0.7.2" :sha "0361dde"}}
:ns-default build}
:profile {:extra-paths ["dev/clj"]
:deps {software.amazon.awssdk/sqs {:mvn/version "2.17.100"}
software.amazon.awssdk/sso {:mvn/version "2.17.100"}}}}}
In src/clj/tax/core.clj
(ns tax.core
(:import (software.amazon.awssdk.services.s3 S3Client)
(software.amazon.awssdk.services.s3.model ListBucketsRequest))
(:gen-class
:methods [^:static [calculationsHandler [Object] Object]]))
(defn list-buckets []
(let [s3 (-> (S3Client/builder) (.build))
req (-> (ListBucketsRequest/builder) (.build))]
(.listBuckets s3 req)))
(defn -calculationsHandler [event]
(str (list-buckets)))
And in template.yml, add some buckets and allow the Lambdas to access them:
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- "AWS::Serverless-2016-10-31"
Resources:
RunClojureScriptCalculationsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub "${AWS::StackName}-run-calcs-queue-cljs"
VisibilityTimeout: 5400
RunClojureCalculationsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub "${AWS::StackName}-run-calcs-queue-clj"
VisibilityTimeout: 5400
TransactionsBucket:
Type: AWS::S3::Bucket
CalculationsBucket:
Type: AWS::S3::Bucket
RunCalculationsCLJ:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-run-calcs-clj"
Handler: tax.core::::calculationsHandler
Runtime: java11
CodeUri: target/tax-engine-0.1.0-standalone.jar
Timeout: 900
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
Environment:
Variables:
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunClojureCalculationsQueue.Arn
BatchSize: 1
RunCalculationsCLJS:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-run-calcs-cljs"
Handler: index.handler
Runtime: nodejs14.x
CodeUri: target/lambda/calcs
Timeout: 900
MemorySize: 128
Policies:
- AWSLambdaBasicExecutionRole
- S3ReadPolicy:
BucketName: !Ref TransactionsBucket
- S3WritePolicy:
BucketName: !Ref CalculationsBucket
Environment:
Variables:
TRANSACTIONS_BUCKET: !Ref TransactionsBucket
CALCULATIONS_BUCKET: !Ref CalculationsBucket
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt RunClojureScriptCalculationsQueue.Arn
BatchSize: 1
Deploying, and running tests in the console, I get:
Clojure:
Init duration: 2864.75 ms Duration: 11055.58 ms
ClojureScript:
Init Duration: 494.70 ms Duration: 1114.63 ms
I was not expecting that! The init duration for CLJS went up significantly while CLJ stayed the same, but the difference in execution time is surprising! I'll run my SQS blaster on each of the two to get more rigorous results.


Summarizing the results:
Clojure:
Average Init Duration: 3037.3744 Average Duration: 712.381
ClojureScript:
Average Init Duration: 454.599 Average Duration: 144.5778
These results are very interesting in several ways. The difference between that first invocation of the CLJ version and the average is quite stark. I need to investigate what is going on there. Also, I would have expected the CLJ version to be much faster after load than the CLJS version, but it is nearly 5 times slower. Another surprising thing is that just adding the dependency on 'aws-sdk' and statically initializing the S3 client increased the init duration by more than double.
This leaves me with several things to investigate:
I'll investigate 3 in my next post.