Skip to content

Instantly share code, notes, and snippets.

@bsless
Created August 31, 2023 12:44
Show Gist options
  • Save bsless/fb79601eb2bfdee85ebf4663dbc7bb1b to your computer and use it in GitHub Desktop.
Save bsless/fb79601eb2bfdee85ebf4663dbc7bb1b to your computer and use it in GitHub Desktop.
Clojure app startup performance

Why

Simple experiment to test the effects of different techniques and options on application start up time

Class Data Sharing (CDS)

The goal of CDS is to reduce the startup time of the JVM by loading from a pre-processed archive of Java classes and JVM metadata that is used during the initialization process. https://dev.java/learn/jvm/cds-appcds/

Clojure compiler options

see

Meta elision

To decrease class size and make classloading faster, meta can be elided.

Direct linking

Direct linking can be used to replace this indirection with a direct static invocation of the function instead. This will result in faster var invocation. Additionally, the compiler can remove unused vars from class initialization and direct linking will make many more vars unused. Typically this results in smaller class sizes and faster startup times.

Technique

Results

The table below displays hyperfine results for mean application start up time with different options enabled and speed up relative to the baseline

cds direct elide mean ms stddev ms speedup%
false false false 1431 18 0
false false true 1422 33 .63
false true false 1388 18 3
false true true 1379 17 3.6
true false false 992.9 16.4 30
true false true 992.4 20.1 30
true true false 972.6 12.5 32
true true true 959.4 12.3 33

Discussion

CDS has significant flat speed up effect. Consider using it if you have to shave off your start up time

For big applications direct linking and meta elision have small effect on start up time.

They have other performance benefits, especially direct linking, on application stready state performance.

The effort required to enjoy these improvements was very little

Implementation

hack uber opts

(defn- uber-opts [{:keys [elide direct] :as opts}]
  (-> opts
      (assoc
       :lib lib :main main
       :uber-file (format "target/%s-standalone.jar" lib)
       :basis (b/create-basis {})
       :class-dir class-dir
       :src-dirs ["src"])
      (cond->
          direct (assoc-in [:compile-opts :direct-linking] true)
          elide (assoc-in [:compile-opts :elide-meta] [:doc :file :line]))))

hack main

(defn -main
  [& [port]]
  (let [port (or port (get (System/getenv) "PORT" 8080))
        port (cond-> port (string? port) Integer/parseInt)]
    (println "Starting up on port" port)
    ;; start the web server and application:
    (-> (component/start (new-system port false))
        ;; then put it into the atom so we can get at it from a REPL
        ;; connected to this application:
        (->> (reset! repl-system))
        ;; then wait "forever" on the promise created:
        #_#_#_:web-server :shutdown deref))
  (System/exit 0))

run script

#!/usr/bin/env sh

for direct in true false;
do
    for elide in true false;
    do
        echo direct $direct elide $elide
        clojure -T:build ci :elide $elide :direct $direct && \
            java \
                -XX:ArchiveClassesAtExit=archive.jsa \
                -jar target/usermanager/example-standalone.jar && \
            hyperfine 'java -XX:SharedArchiveFile=archive.jsa -jar target/usermanager/example-standalone.jar' && \
            hyperfine 'java -jar target/usermanager/example-standalone.jar'
    done
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment