├── .scalafmt.conf ├── project ├── build.properties ├── metals.sbt ├── plugins.sbt └── Dependencies.scala ├── src └── main │ ├── resources │ ├── META-INF │ │ └── native-image │ │ │ ├── co.innerproduct │ │ │ └── ping │ │ │ │ ├── reflection.json │ │ │ │ ├── resource-config.json │ │ │ │ ├── native-image.properties │ │ │ │ └── reflect-config.json │ │ │ └── org.scala-lang │ │ │ └── scala-lang │ │ │ └── native-image.properties │ └── logback.xml │ └── scala │ └── ping │ ├── Main.scala │ ├── PingRoutes.scala │ ├── Ping.scala │ └── PingServer.scala ├── out └── Dockerfile ├── docker └── Dockerfile ├── sustained-benchmark.sh ├── README.md ├── startup-benchmark.sh ├── .gitignore ├── jvm-tuning-benchmark.sh └── analysis.py /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.3.2" 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.3 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/co.innerproduct/ping/reflection.json: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | ] 4 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/org.scala-lang/scala-lang/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = --initialize-at-build-time=scala.runtime.Statics$VM 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/co.innerproduct/ping/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources":[ 3 | {"pattern":"logback.xml"} 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/co.innerproduct/ping/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = -H:+ReportExceptionStackTraces --allow-incomplete-classpath --no-fallback 2 | -------------------------------------------------------------------------------- /out/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11.3 2 | COPY ping-server /opt/ping-server/ping-server 3 | RUN chmod +x /opt/ping-server/ping-server 4 | EXPOSE 8080 5 | ENTRYPOINT ["/opt/ping-server/ping-server"] 6 | -------------------------------------------------------------------------------- /project/metals.sbt: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT! This file is auto-generated. 2 | // This file enables sbt-bloop to create bloop config files. 3 | 4 | addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.0-RC1") 5 | -------------------------------------------------------------------------------- /src/main/scala/ping/Main.scala: -------------------------------------------------------------------------------- 1 | package co.innerproduct 2 | package ping 3 | 4 | import cats.effect.{ExitCode, IOApp} 5 | import cats.implicits._ 6 | 7 | object Main extends IOApp { 8 | def run(args: List[String]) = 9 | PingServer.stream.compile.drain.as(ExitCode.Success) 10 | } 11 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4") 2 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.10") 3 | addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") 4 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") 5 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1") 6 | -------------------------------------------------------------------------------- /src/main/scala/ping/PingRoutes.scala: -------------------------------------------------------------------------------- 1 | package co.innerproduct 2 | package ping 3 | 4 | import cats.effect.IO 5 | import org.http4s.HttpRoutes 6 | import org.http4s.dsl.Http4sDsl 7 | 8 | object PingRoutes { 9 | val dsl = new Http4sDsl[IO] {} 10 | 11 | val pingRoutes: HttpRoutes[IO] = { 12 | import dsl._ 13 | HttpRoutes.of[IO] { 14 | case GET -> Root / "ping" / message => 15 | for { 16 | pong <- Ping.ping(message) 17 | resp <- Ok(pong) 18 | } yield resp 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to build an image that runs the GraalVM native-image compiler. We 2 | # need to do this because native-image only builds on the platform on which it 3 | # runs. Hence if we want to build Linux binaries on a different machine we need 4 | # to create a Linux VM that does this. This is what we are doing here. 5 | # 6 | # To build this image, run: 7 | # 8 | # docker build -t inner-product/graalvm-native-image . 9 | # 10 | ARG GRAAL_VERSION=19.3.1-java11 11 | FROM oracle/graalvm-ce:${GRAAL_VERSION} 12 | WORKDIR /opt/native-image 13 | RUN gu install native-image 14 | ENTRYPOINT ["native-image"] 15 | -------------------------------------------------------------------------------- /src/main/scala/ping/Ping.scala: -------------------------------------------------------------------------------- 1 | package co.innerproduct 2 | package ping 3 | 4 | import cats.effect.IO 5 | import io.circe.{Encoder, Json} 6 | import org.http4s.EntityEncoder 7 | import org.http4s.circe._ 8 | 9 | object Ping { 10 | 11 | def ping(message: String): IO[Pong] = 12 | IO(Pong(message)) 13 | 14 | final case class Pong(message: String) extends AnyVal 15 | object Pong { 16 | implicit val pongEncoder: Encoder[Pong] = new Encoder[Pong] { 17 | final def apply(a: Pong): Json = Json.obj( 18 | ("message", Json.fromString(a.message)) 19 | ) 20 | } 21 | 22 | implicit val pongEntityEncoder: EntityEncoder[IO, Pong] = 23 | jsonEncoderOf[IO, Pong] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | true 9 | 10 | [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/ping/PingServer.scala: -------------------------------------------------------------------------------- 1 | package co.innerproduct 2 | package ping 3 | 4 | import cats.effect.{ContextShift, IO, Timer} 5 | import fs2.Stream 6 | import org.http4s.client.blaze.BlazeClientBuilder 7 | import org.http4s.implicits._ 8 | import org.http4s.server.blaze.BlazeServerBuilder 9 | import org.http4s.server.middleware.Logger 10 | import scala.concurrent.ExecutionContext.global 11 | 12 | object PingServer { 13 | def stream(implicit T: Timer[IO], C: ContextShift[IO]): Stream[IO, Nothing] = { 14 | for { 15 | client <- BlazeClientBuilder[IO](global).stream 16 | 17 | httpApp = (PingRoutes.pingRoutes).orNotFound 18 | 19 | // With Middlewares in place 20 | finalHttpApp = Logger.httpApp(true, true)(httpApp) 21 | 22 | exitCode <- BlazeServerBuilder[IO] 23 | .bindHttp(8080, "0.0.0.0") 24 | .withHttpApp(finalHttpApp) 25 | .serve 26 | } yield exitCode 27 | }.drain 28 | } 29 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | 4 | object Dependencies { 5 | // Library Versions 6 | val catsVersion = "2.1.0" 7 | val catsEffectVersion = "2.0.0" 8 | val circeVersion = "0.12.3" 9 | 10 | val http4sVersion = "0.21.0" 11 | 12 | val logbackVersion = "1.2.3" 13 | val janinoVersion = "3.1.0" 14 | 15 | val miniTestVersion = "2.7.0" 16 | val scalaCheckVersion = "1.14.1" 17 | 18 | 19 | // Libraries 20 | val catsEffect = Def.setting("org.typelevel" %% "cats-effect" % catsEffectVersion) 21 | val catsCore = Def.setting("org.typelevel" %% "cats-core" % catsVersion) 22 | 23 | val miniTest = Def.setting("io.monix" %% "minitest" % miniTestVersion % "test") 24 | val miniTestLaws = Def.setting("io.monix" %% "minitest-laws" % miniTestVersion % "test") 25 | 26 | val http4sBlazeServer = Def.setting("org.http4s" %% "http4s-blaze-server" % http4sVersion) 27 | val http4sBlazeClient = Def.setting("org.http4s" %% "http4s-blaze-client" % http4sVersion) 28 | val http4sCirce = Def.setting("org.http4s" %% "http4s-circe" % http4sVersion) 29 | val http4sDsl = Def.setting("org.http4s" %% "http4s-dsl" % http4sVersion) 30 | 31 | val logback = Def.setting("ch.qos.logback" % "logback-classic" % logbackVersion) 32 | val janino = Def.setting("org.codehaus.janino" % "janino" % janinoVersion) 33 | 34 | val circe = Def.setting("io.circe" %% "circe-generic" % circeVersion) 35 | } 36 | -------------------------------------------------------------------------------- /sustained-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Measure sustained performance under load 4 | 5 | # number of iterations to run per server 6 | ITERATIONS=10000 7 | # maximum number of concurrent requests 8 | CONCURRENCY=50 9 | # URL to test 10 | URL=http://localhost:8080/ping/hi 11 | # ab stops after timeout seconds regardless of how many requests it has sent 12 | TIMEOUT=300 13 | 14 | 15 | echo "Benchmarking Native Image server" 16 | ./out/ping-server.local > local-log.txt & 17 | SERVER_PID=$! 18 | SLEEP 5 19 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e native-image-sustained.csv ${URL} 20 | kill ${SERVER_PID} 21 | # Give some time for shutdown to complete 22 | sleep 5 23 | 24 | 25 | echo "Benchmarking cold JVM server" 26 | java -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar > jvm-log.txt & 27 | SERVER_PID=$! 28 | SLEEP 5 29 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-cold-sustained.csv ${URL} 30 | # Give some time for GC to run 31 | sleep 5 32 | 33 | echo "Benchmarking warm JVM server" 34 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-1.csv ${URL} 35 | sleep 5 36 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-2.csv ${URL} 37 | sleep 5 38 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-3.csv ${URL} 39 | sleep 5 40 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-4.csv ${URL} 41 | sleep 5 42 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-5.csv ${URL} 43 | sleep 5 44 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-6.csv ${URL} 45 | sleep 5 46 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-7.csv ${URL} 47 | sleep 5 48 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-warm-sustained-8.csv ${URL} 49 | kill ${SERVER_PID} 50 | # Give some time for shutdown to complete 51 | sleep 5 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http4s Executable via GraalVM Native Image 2 | 3 | This repository demonstrates how to compile an http4s application to a native executable using GraalVM's Native Image. The accompanying [blog post](https://www.inner-product.com/posts/serverless-scala-services-with-graalvm/) has all the details. 4 | 5 | To quickly get going: 6 | 7 | 1. Having installed GraalVM Native Image run the `nativeImageLocal` task to generate an executable. 8 | 9 | 2. Optionally, run `startup-benchmark.sh` and `sustained-benchmark.sh` to 10 | benchmark against the JVM, and `analysis.py` to generate graphs. 11 | 12 | 13 | To build a Linux executable: 14 | 15 | 1. Build the Docker image in the `docker` directory 16 | 17 | ``` sh 18 | cd docker 19 | docker build -t inner-product/graalvm-native-image . 20 | ``` 21 | 22 | 2. Run the `nativeImage` task from sbt. The result will be a Linux executable. 23 | 24 | 25 | ## License 26 | 27 | Distributed under the MIT license. 28 | 29 | Copyright 2020 Inner Product LLC 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | -------------------------------------------------------------------------------- /startup-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Measure the startup time (time to first response) for Native Image and JVM server 4 | # 5 | # Modified from https://sites.google.com/a/athaydes.com/renato-athaydes/posts/a7mbnative-imagejavaappthatrunsin30msandusesonly4mbofram 6 | 7 | # command that tests that the server is accepting connections 8 | CMD="curl localhost:8080/ping/hi >& /dev/null" 9 | 10 | RESULTS_FILE=ping-server-native.csv 11 | 12 | rm $RESULTS_FILE 13 | echo "time (ms),mem (kb)" >> $RESULTS_FILE 14 | for N in {1..100} 15 | do 16 | START_TIME=$(gdate +%s%3N) 17 | # start the server 18 | ./out/ping-server.local & 19 | SERVER_PID=$! 20 | 21 | STEP=0.001 # sleep between tries, in seconds 22 | TRIES=500 23 | eval ${CMD} 24 | while [[ $? -ne 0 ]]; do 25 | ((TRIES--)) 26 | echo -ne "Tries left: $TRIES"\\r 27 | if [[ TRIES -eq 0 ]]; then 28 | echo "Server not started within timeout" 29 | exit 1 30 | fi 31 | sleep ${STEP} 32 | eval ${CMD} 33 | done 34 | 35 | END_TIME=$(gdate +%s%3N) 36 | TIME=$(($END_TIME - $START_TIME)) 37 | MEM=$(ps -o rss= -p "$SERVER_PID") 38 | echo "Server connected in $TIME ms" 39 | echo "Memory usage is $MEM" 40 | printf "%d,%s\n" $TIME $MEM >> $RESULTS_FILE 41 | 42 | kill ${SERVER_PID} 43 | done 44 | 45 | 46 | RESULTS_FILE=ping-server-jvm.csv 47 | 48 | rm $RESULTS_FILE 49 | echo "time (ms),mem (kb)" >> $RESULTS_FILE 50 | for N in {1..100} 51 | do 52 | START_TIME=$(gdate +%s%3N) 53 | # start the server 54 | java -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar & 55 | SERVER_PID=$! 56 | 57 | STEP=0.001 # sleep between tries, in seconds 58 | TRIES=500 59 | eval ${CMD} 60 | while [[ $? -ne 0 ]]; do 61 | ((TRIES--)) 62 | echo -ne "Tries left: $TRIES"\\r 63 | if [[ TRIES -eq 0 ]]; then 64 | echo "Server not started within timeout" 65 | exit 1 66 | fi 67 | sleep ${STEP} 68 | eval ${CMD} 69 | done 70 | 71 | END_TIME=$(gdate +%s%3N) 72 | TIME=$(($END_TIME - $START_TIME)) 73 | MEM=$(ps -o rss= -p "$SERVER_PID") 74 | echo "Server connected in $TIME ms" 75 | echo "Memory usage is $MEM" 76 | printf "%d,%s\n" $TIME $MEM >> $RESULTS_FILE 77 | 78 | kill ${SERVER_PID} 79 | # Without this sleep the port will occasionally not have been released and the next run will fail 80 | sleep 0.1 81 | done 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/scala,intellij,eclipse,sbt 2 | 3 | ### Scala ### 4 | *.class 5 | *.log 6 | 7 | # sbt specific 8 | .cache 9 | .cache-main 10 | .history 11 | .lib/ 12 | dist/* 13 | target/ 14 | lib_managed/ 15 | src_managed/ 16 | project/boot/ 17 | project/plugins/project/ 18 | project/target 19 | project/project 20 | 21 | # Scala-IDE specific 22 | .scala_dependencies 23 | .worksheet 24 | 25 | # Documentation intermediate files 26 | docs/src/main/paradox 27 | docs/src/main/mdoc/api 28 | 29 | 30 | ### SublimeText ### 31 | # cache files for sublime text 32 | *.tmlanguage.cache 33 | *.tmPreferences.cache 34 | *.stTheme.cache 35 | 36 | # workspace files are user-specific 37 | *.sublime-workspace 38 | 39 | # project files should be checked into the repository, unless a significant 40 | # proportion of contributors will probably not be using SublimeText 41 | # *.sublime-project 42 | 43 | # sftp configuration file 44 | sftp-config.json 45 | 46 | 47 | ### Intellij ### 48 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 49 | 50 | *.iml 51 | 52 | ## Directory-based project format: 53 | .idea/ 54 | # if you remove the above rule, at least ignore the following: 55 | 56 | # User-specific stuff: 57 | # .idea/workspace.xml 58 | # .idea/tasks.xml 59 | # .idea/dictionaries 60 | 61 | # Sensitive or high-churn files: 62 | # .idea/dataSources.ids 63 | # .idea/dataSources.xml 64 | # .idea/sqlDataSources.xml 65 | # .idea/dynamic.xml 66 | # .idea/uiDesigner.xml 67 | 68 | # Gradle: 69 | # .idea/gradle.xml 70 | # .idea/libraries 71 | 72 | # Mongo Explorer plugin: 73 | # .idea/mongoSettings.xml 74 | 75 | ## File-based project format: 76 | *.ipr 77 | *.iws 78 | 79 | ## Plugin-specific files: 80 | 81 | # IntelliJ 82 | /out/ 83 | 84 | # mpeltonen/sbt-idea plugin 85 | .idea_modules/ 86 | 87 | # JIRA plugin 88 | atlassian-ide-plugin.xml 89 | 90 | # Crashlytics plugin (for Android Studio and IntelliJ) 91 | com_crashlytics_export_strings.xml 92 | crashlytics.properties 93 | crashlytics-build.properties 94 | 95 | 96 | ### Eclipse ### 97 | *.pydevproject 98 | .metadata 99 | .gradle 100 | bin/ 101 | tmp/ 102 | *.tmp 103 | *.bak 104 | *.swp 105 | *~.nib 106 | local.properties 107 | .settings/ 108 | .loadpath 109 | 110 | # Eclipse Core 111 | .project 112 | 113 | # External tool builders 114 | .externalToolBuilders/ 115 | 116 | # Locally stored "Eclipse launch configurations" 117 | *.launch 118 | 119 | # CDT-specific 120 | .cproject 121 | 122 | # JDT-specific (Eclipse Java Development Tools) 123 | .classpath 124 | 125 | # Java annotation processor (APT) 126 | .factorypath 127 | 128 | # PDT-specific 129 | .buildpath 130 | 131 | # sbteclipse plugin 132 | .target 133 | 134 | # TeXlipse plugin 135 | .texlipse 136 | 137 | # Metals and Bloop 138 | .metals 139 | .bloop 140 | -------------------------------------------------------------------------------- /jvm-tuning-benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Generate data for JVM tuning experiments 4 | 5 | # number of iterations to run per server 6 | ITERATIONS=10000 7 | # maximum number of concurrent requests 8 | CONCURRENCY=50 9 | # URL to test 10 | URL=http://localhost:8080/ping/hi 11 | # ab stops after timeout seconds regardless of how many requests it has sent 12 | TIMEOUT=300 13 | 14 | echo "Untuned JVM server" 15 | echo "Warming up" 16 | java -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar > jvm-log.txt & 17 | SERVER_PID=$! 18 | SLEEP 5 19 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 20 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 21 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 22 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 23 | # Give some time for GC to run 24 | sleep 5 25 | echo "-----------------------------------------" 26 | echo "Untuned JVM Benchmark Run" 27 | echo "-----------------------------------------" 28 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-untuned.csv ${URL} 29 | kill ${SERVER_PID} 30 | sleep 5 31 | 32 | 33 | echo "GC tuned JVM server" 34 | echo "Warming up" 35 | java -XX:MaxGCPauseMillis=200 -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar > jvm-log.txt & 36 | SERVER_PID=$! 37 | SLEEP 5 38 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 39 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 40 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 41 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 42 | # Give some time for GC to run 43 | sleep 5 44 | echo "-----------------------------------------" 45 | echo "GC Tuned JVM Benchmark Run" 46 | echo "-----------------------------------------" 47 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-gc-tuned.csv ${URL} 48 | kill ${SERVER_PID} 49 | sleep 5 50 | 51 | 52 | echo "Compiler tuned JVM server" 53 | echo "Warming up" 54 | java -Dgraal.TrivialInliningSize=21 -Dgraal.MaximumInliningSize=450 -Dgraal.SmallCompiledLowLevelGraphSize=550 -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar > jvm-log.txt & 55 | SERVER_PID=$! 56 | SLEEP 5 57 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 58 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 59 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 60 | ab -dSq -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 61 | # Give some time for GC to run 62 | sleep 5 63 | echo "-----------------------------------------" 64 | echo "Compiler Tuned JVM Benchmark Run" 65 | echo "-----------------------------------------" 66 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-compiler-tuned.csv ${URL} 67 | kill ${SERVER_PID} 68 | sleep 5 69 | 70 | 71 | echo "Compiler + GC tuned JVM server" 72 | echo "Warming up" 73 | java -XX:MaxGCPauseMillis=200 -Dgraal.TrivialInliningSize=21 -Dgraal.MaximumInliningSize=450 -Dgraal.SmallCompiledLowLevelGraphSize=550 -jar target/scala-2.13/http4s-native-image-assembly-0.1.0-SNAPSHOT.jar > jvm-log.txt & 74 | SERVER_PID=$! 75 | SLEEP 5 76 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 77 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 78 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 79 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} ${URL} 80 | # Give some time for GC to run 81 | sleep 5 82 | echo "-----------------------------------------" 83 | echo "Both Tuned JVM Benchmark Run" 84 | echo "-----------------------------------------" 85 | ab -n ${ITERATIONS} -c ${CONCURRENCY} -s ${TIMEOUT} -e jvm-both-tuned.csv ${URL} 86 | kill ${SERVER_PID} 87 | sleep 5 88 | -------------------------------------------------------------------------------- /analysis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Generate graphs of the results given the data generated by the experiment scripts. 4 | # Requires numpy and matplotlib 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | 10 | ## Startup time and memory usage 11 | ## 12 | native_data = np.genfromtxt('ping-server-native.csv', delimiter=',', skip_header=1) 13 | native_time = native_data[:,0] 14 | native_mem = native_data[:, 1] 15 | 16 | jvm_data = np.genfromtxt('ping-server-jvm.csv', delimiter=',', skip_header=1) 17 | jvm_time = jvm_data[:,0] 18 | jvm_mem = jvm_data[:, 1] 19 | 20 | plt.plot(native_time, label='Native Image') 21 | plt.plot(jvm_time, label='JVM') 22 | plt.ylabel('Startup time (ms)') 23 | plt.xlabel('Run') 24 | plt.title("Time from Start to first HTTP Response") 25 | plt.legend() 26 | plt.savefig('startup-time.png') 27 | plt.show() 28 | 29 | plt.plot(native_mem, label='Native Image') 30 | plt.plot(jvm_mem, label='JVM') 31 | plt.ylabel('Memory usage (KB)') 32 | plt.xlabel('Run') 33 | plt.title("Resident Set Size after first HTTP Response") 34 | plt.legend() 35 | plt.savefig('resident-set-size.png') 36 | plt.show() 37 | 38 | 39 | ## Response times under sustained load 40 | ## 41 | native_image_sustained = np.genfromtxt('native-image-sustained.csv', delimiter=',', skip_header=1) 42 | jvm_cold_sustained = np.genfromtxt('jvm-cold-sustained.csv', delimiter=',', skip_header=1) 43 | jvm_warm_sustained = np.genfromtxt('jvm-warm-sustained-8.csv', delimiter=',', skip_header=1) 44 | 45 | plt.plot(native_image_sustained[0:99, 0], native_image_sustained[0:99,1], label="Native Image") 46 | plt.plot(jvm_cold_sustained[0:99,0], jvm_cold_sustained[0:99,1], label="JVM (Cold)") 47 | plt.plot(jvm_warm_sustained[0:99,0], jvm_warm_sustained[0:99,1], label="JVM (Warm)") 48 | plt.ylabel("Response time (ms)") 49 | plt.xlabel("Percentile") 50 | plt.title("0-99 Percentile response times over 10000 requests") 51 | plt.legend() 52 | plt.savefig('lower-sustained-response-time.png') 53 | plt.show() 54 | 55 | plt.plot(native_image_sustained[95:100, 0], native_image_sustained[95:100,1], label="Native Image") 56 | plt.plot(jvm_cold_sustained[95:100,0], jvm_cold_sustained[95:100,1], label="JVM (Cold)") 57 | plt.plot(jvm_warm_sustained[95:100,0], jvm_warm_sustained[95:100,1], label="JVM (Warm)") 58 | plt.ylabel("Response time (ms)") 59 | plt.xlabel("Percentile") 60 | plt.title("95-100 Percentile response times over 10000 requests") 61 | plt.legend() 62 | plt.savefig('upper-sustained-response-time.png') 63 | plt.show() 64 | 65 | 66 | ## JVM Response time over time 67 | jvm_warm_sustained_1 = np.genfromtxt('jvm-warm-sustained-1.csv', delimiter=',', skip_header=1) 68 | jvm_warm_sustained_2 = np.genfromtxt('jvm-warm-sustained-2.csv', delimiter=',', skip_header=1) 69 | jvm_warm_sustained_3 = np.genfromtxt('jvm-warm-sustained-3.csv', delimiter=',', skip_header=1) 70 | jvm_warm_sustained_4 = np.genfromtxt('jvm-warm-sustained-4.csv', delimiter=',', skip_header=1) 71 | jvm_warm_sustained_5 = np.genfromtxt('jvm-warm-sustained-5.csv', delimiter=',', skip_header=1) 72 | jvm_warm_sustained_6 = np.genfromtxt('jvm-warm-sustained-6.csv', delimiter=',', skip_header=1) 73 | jvm_warm_sustained_7 = np.genfromtxt('jvm-warm-sustained-7.csv', delimiter=',', skip_header=1) 74 | jvm_warm_sustained_8 = np.genfromtxt('jvm-warm-sustained-8.csv', delimiter=',', skip_header=1) 75 | 76 | plt.plot(jvm_warm_sustained_1[0:95,0], jvm_warm_sustained_1[0:95,1], label="After 10,000 requests") 77 | plt.plot(jvm_warm_sustained_2[0:95,0], jvm_warm_sustained_2[0:95,1], label="After 20,000 requests") 78 | plt.plot(jvm_warm_sustained_3[0:95,0], jvm_warm_sustained_3[0:95,1], label="After 30,000 requests") 79 | plt.plot(jvm_warm_sustained_4[0:95,0], jvm_warm_sustained_4[0:95,1], label="After 40,000 requests") 80 | plt.plot(jvm_warm_sustained_5[0:95,0], jvm_warm_sustained_5[0:95,1], label="After 50,000 requests") 81 | plt.plot(jvm_warm_sustained_6[0:95,0], jvm_warm_sustained_6[0:95,1], label="After 60,000 requests") 82 | plt.plot(jvm_warm_sustained_7[0:95,0], jvm_warm_sustained_7[0:95,1], label="After 70,000 requests") 83 | plt.plot(jvm_warm_sustained_8[0:95,0], jvm_warm_sustained_8[0:95,1], label="After 80,000 requests") 84 | plt.ylabel("Response time (ms)") 85 | plt.xlabel("Percentile") 86 | plt.title("JVM 0-95 Percentile response times over time") 87 | plt.legend() 88 | plt.savefig('warming-sustained-response-time.png') 89 | plt.show() 90 | 91 | 92 | ## Response times under sustained load for various JVM settings 93 | 94 | jvm_untuned = np.genfromtxt('jvm-untuned.csv', delimiter=',', skip_header=1) 95 | jvm_gc_tuned = np.genfromtxt('jvm-gc-tuned.csv', delimiter=',', skip_header=1) 96 | jvm_compiler_tuned = np.genfromtxt('jvm-compiler-tuned.csv', delimiter=',', skip_header=1) 97 | jvm_both_tuned = np.genfromtxt('jvm-both-tuned.csv', delimiter=',', skip_header=1) 98 | 99 | plt.plot(jvm_untuned[0:99, 0], jvm_untuned[0:99,1], label="Untuned JVM") 100 | plt.plot(jvm_gc_tuned[0:99,0], jvm_gc_tuned[0:99,1], label="GC tuned JVM") 101 | plt.plot(jvm_compiler_tuned[0:99,0], jvm_compiler_tuned[0:99,1], label="Compiler tuned JVM") 102 | plt.plot(jvm_both_tuned[0:99,0], jvm_both_tuned[0:99,1], label="Both tuned JVM") 103 | plt.ylabel("Response time (ms)") 104 | plt.xlabel("Percentile") 105 | plt.title("0-99 Percentile response times over 10000 requests") 106 | plt.legend() 107 | plt.savefig('lower-tuned-response-time.png') 108 | plt.show() 109 | 110 | plt.plot(jvm_untuned[95:100, 0], jvm_untuned[95:100,1], label="Untuned JVM") 111 | plt.plot(jvm_gc_tuned[95:100,0], jvm_gc_tuned[95:100,1], label="GC tuned JVM") 112 | plt.plot(jvm_compiler_tuned[95:100,0], jvm_compiler_tuned[95:100,1], label="Compiler tuned JVM") 113 | plt.plot(jvm_both_tuned[95:100,0], jvm_both_tuned[95:100,1], label="Both tuned JVM") 114 | plt.ylabel("Response time (ms)") 115 | plt.xlabel("Percentile") 116 | plt.title("95-100 Percentile response times over 10000 requests") 117 | plt.legend() 118 | plt.savefig('upper-tuned-response-time.png') 119 | plt.show() 120 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/co.innerproduct/ping/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org.slf4j.impl.StaticLoggerBinder", 4 | "allDeclaredConstructors": true 5 | }, 6 | { 7 | "name": "ch.qos.logback.classic.pattern.DateConverter", 8 | "allDeclaredConstructors": true 9 | }, 10 | { 11 | "name": "ch.qos.logback.classic.pattern.MessageConverter", 12 | "allDeclaredConstructors": true 13 | }, 14 | { 15 | "name": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", 16 | "allDeclaredConstructors": true 17 | }, 18 | { 19 | "name": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", 20 | "allDeclaredConstructors": true 21 | }, 22 | { 23 | "name": "ch.qos.logback.classic.pattern.ContextNameConverter", 24 | "allDeclaredConstructors": true 25 | }, 26 | { 27 | "name": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", 28 | "allDeclaredConstructors": true 29 | }, 30 | { 31 | "name": "ch.qos.logback.classic.pattern.LoggerConverter", 32 | "allDeclaredConstructors": true 33 | }, 34 | { 35 | "name": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", 36 | "allDeclaredConstructors": true 37 | }, 38 | { 39 | "name": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", 40 | "allDeclaredConstructors": true 41 | }, 42 | { 43 | "name": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", 44 | "allDeclaredConstructors": true 45 | }, 46 | { 47 | "name": "ch.qos.logback.core.pattern.color.RedCompositeConverter", 48 | "allDeclaredConstructors": true 49 | }, 50 | { 51 | "name": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", 52 | "allDeclaredConstructors": true 53 | }, 54 | { 55 | "name": "ch.qos.logback.classic.pattern.PropertyConverter", 56 | "allDeclaredConstructors": true 57 | }, 58 | { 59 | "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", 60 | "allDeclaredConstructors": true 61 | }, 62 | { 63 | "name": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", 64 | "allDeclaredConstructors": true 65 | }, 66 | { 67 | "name": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", 68 | "allDeclaredConstructors": true 69 | }, 70 | { 71 | "name": "ch.qos.logback.classic.pattern.LevelConverter", 72 | "allDeclaredConstructors": true 73 | }, 74 | { 75 | "name": "ch.qos.logback.core.pattern.IdentityCompositeConverter", 76 | "allDeclaredConstructors": true 77 | }, 78 | { 79 | "name": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", 80 | "allDeclaredConstructors": true 81 | }, 82 | { 83 | "name": "ch.qos.logback.classic.pattern.MarkerConverter", 84 | "allDeclaredConstructors": true 85 | }, 86 | { 87 | "name": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", 88 | "allDeclaredConstructors": true 89 | }, 90 | { 91 | "name": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", 92 | "allDeclaredConstructors": true 93 | }, 94 | { 95 | "name": "ch.qos.logback.classic.pattern.RelativeTimeConverter", 96 | "allDeclaredConstructors": true 97 | }, 98 | { 99 | "name": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", 100 | "allDeclaredConstructors": true 101 | }, 102 | { 103 | "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", 104 | "allDeclaredConstructors": true 105 | }, 106 | { 107 | "name": "ch.qos.logback.classic.pattern.LineOfCallerConverter", 108 | "allDeclaredConstructors": true 109 | }, 110 | { 111 | "name": "ch.qos.logback.classic.pattern.FileOfCallerConverter", 112 | "allDeclaredConstructors": true 113 | }, 114 | { 115 | "name": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", 116 | "allDeclaredConstructors": true 117 | }, 118 | { 119 | "name": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", 120 | "allDeclaredConstructors": true 121 | }, 122 | { 123 | "name": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", 124 | "allDeclaredConstructors": true 125 | }, 126 | { 127 | "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", 128 | "allDeclaredConstructors": true 129 | }, 130 | { 131 | "name": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", 132 | "allDeclaredConstructors": true 133 | }, 134 | { 135 | "name": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", 136 | "allDeclaredConstructors": true 137 | }, 138 | { 139 | "name": "ch.qos.logback.classic.pattern.MDCConverter", 140 | "allDeclaredConstructors": true 141 | }, 142 | { 143 | "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", 144 | "allDeclaredConstructors": true 145 | }, 146 | { 147 | "name": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", 148 | "allDeclaredConstructors": true 149 | }, 150 | { 151 | "name": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", 152 | "allDeclaredConstructors": true 153 | }, 154 | { 155 | "name": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", 156 | "allDeclaredConstructors": true 157 | }, 158 | { 159 | "name": "ch.qos.logback.classic.pattern.ThreadConverter", 160 | "allDeclaredConstructors": true 161 | }, 162 | { 163 | "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", 164 | "allDeclaredConstructors": true 165 | }, 166 | { 167 | "name": "ch.qos.logback.classic.encoder.PatternLayoutEncoder", 168 | "allPublicMethods":true, 169 | "allDeclaredConstructors": true 170 | }, 171 | { 172 | "name": "ch.qos.logback.core.ConsoleAppender", 173 | "allPublicMethods":true, 174 | "allDeclaredConstructors": true 175 | }, 176 | { 177 | "name": "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", 178 | "allDeclaredConstructors": true 179 | } 180 | ] 181 | --------------------------------------------------------------------------------