GraalVM: Fast,
Polyglot, Native
21.11.2018 // CODEMOTION BERLIN
Jan Stępień
@janstepien
janstepien.com
JAN STĘPIEŃ
Senior Consultant
jan.stepien@innoq.com
Functional Programming
Property-Based Testing
Architecture and Development
Some background
Ahead-of-time compilation
*.java *.clj *.scala
*.class
Ahead-of-time compilation
*.class
Just-in-time compilation
Native machine code
HotSpot
JIT compiling since 1999
JEP 243
JVM Compiler Interface
JD
K
9
Graal
A compiler written in Java
graalvm.org
Three main target groups
JVM engineers
Authors of languages
Programmers and end users
Architecture
Truffle
TruffleRuby
GraalVM
JVMCI
GraalJVM
Three main target groups
JVM engineers
Authors of languages
Programmers and end users
Polyglot VM and tooling
Faster execution
Native compilation
Programmers and end users
Native Java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("👋 Berlin");
}
}
$ time java HelloWorld
👋 Berlin
0.14 real 0.13 user 0.03 sys
29M maximum resident set size
$ native-image HelloWorld
classlist: 3,196.20 ms
⋮
image: 1,693.59 ms
write: 472.40 ms
[total]: 44,197.16 ms
$ time ./HelloWorld
👋 Berlin
0.00 real 0.00 user 0.00 sys
4M maximum resident set size
Ahead-of-time
compilation
Just-in-time
compilation
Slower startup
Higher top speed
High memory usage
Faster startup
Lower top speed
Low memory usage
Native Clojure
$ docker inspect ubuntu 
| jq .[0].Size
(ns pprintin.main
(:require [clojure.pprint :refer [pprint]])
(:gen-class))
(defn -main [& path]
(-> (read *in*)
(get-in (mapv read-string path))
pprint))
$ echo {:a [5 3]} 
| time java -jar pprint.jar :a 1
3
1.51 real
112MB maximum resident set size
$ echo {:a [5 3]} 
| java -XX:TieredStopAtLevel=1 
-jar pprint.jar :a 1
3
1.06 real
96MB maximum resident set size
$ echo {:a [5 3]} 
| time lumo pprint.cljs :a 1
3
0.56 real
128MB maximum resident set size
$ native-image --no-server 
--static -jar pprint.jar
classlist: 3,596.11 ms
(...)
image: 4,671.96 ms
write: 14,763.97 ms
[total]: 122,614.21 ms
$ echo {:a [5 3]} 
| time ./pprint :a 1
3
0.01 real
12MB maximum resident set size
Time Memory
JVM 1.10 s 100 MB
JS 0.60 s 130 MB
Native 0.01 s 12 MB
Command line utilities
(ns json2edn.core
(:require [clojure.data.json :as json]))
(defn -main []
(->> (repeatedly #(json/read *in*
:key-fn keyword
:eof-error? false
:eof-value ::eof))
(take-while #(not= ::eof %))
(run! prn)))
;; See also Taylor Wood’s piece at
;; blog.taylorwood.io/2018/05/02/graal-clojure
$ cat package.json | time json2edn
{:description "Check if a value is an object",
:engines {:node ">=0.10.0"}, :license "MIT",
:repository "sindresorhus/is-obj",
:name "is-obj", :scripts {:test "xo && ava"},
:keywords ["obj" "object" "is" "check" "test"
"type"], :author {:name "Sindre Sorhus",
:email "sindresorhus@gmail.com", :url
"sindresorhus.com"}, :files ["index.js"],
:version “1.0.1"}
0.01 real
10MB maximum resident set size
(require '[spec-provider.provider :as sp])
(def inferred-specs
(sp/infer-specs
[{:a 8 :b "foo" :c :k}
{:a 10 :b "bar" :c "k"}
{:a 1 :b "baz" :c "k"}]
:toy/small-map))
(sp/pprint-specs inferred-specs 'toy ’s)
;; Results in the following being printed
(s/def ::c (s/or :keyword keyword? :string string?))
(s/def ::b string?)
(s/def ::a integer?)
(s/def ::small-map (s/keys :req-un [::a ::b ::c]))
(ns spec-provider.main
(:require [spec-provider.provider :as sp]
[clojure.string :as str]
[clojure.edn :as edn])
(:gen-class))
(defn -main
[spec-name]
(let [kw (as-keyword spec-name)
read-one (edn/read {:eof ::eof} *in*)
samples (sequence
(take-while (complement #{::eof}))
(repeatedly read-one))
specs (sp/infer-specs samples kw)]
(sp/pprint-specs specs (namespace kw) 's))))
$ cat node_modules/*/package.json 
| json2edn | spec-provider :node/package | zprint
;; ...
(s/def ::repository
(s/or :map (s/keys :req-un [::type ::url])
:simple string?))
(s/def ::npm string?)
(s/def ::node string?)
(s/def ::engines (s/keys :req-un [::node] :opt-un [::iojs ::npm]))
(s/def ::description string?)
(s/def ::package
(s/keys :req-un [::description ::name ::repository ::version]
:opt-un [::author ::bin ::browser ::bugs ::contributors
::dependencies ::devDependencies ::engines
::files ::homepage ::icon ::keywords ::license
::main ::maintainers ::optionalDependencies
::peerDependencies ::scripts]))
0.29 real
103MB maximum resident set size
A simple Clojure web app
$ curl -i http://localhost:8080/kv/city -X PUT -d val=Berlin
HTTP/1.1 201 Created
Content-Type: application/octet-stream
Content-Length: 8
Server: http-kit
Date: Mon, 03 Sep 2018 16:04:22 GMT
Berlin
$ curl -i http://localhost:8080/kv/city
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 8
Server: http-kit
Date: Mon, 03 Sep 2018 16:04:27 GMT
Berlin
(ns webkv.main
(:require [org.httpkit.server :as http]
[ring.middleware.defaults
:refer [wrap-defaults
api-defaults]]
[bidi.ring :refer [make-handler]])
(:gen-class))
;; We want to be sure none of our calls relies
;; on reflection. Graal does not support them.
(set! *warn-on-reflection* 1)
;; This is where we store our data.
(def ^String tmpdir
(System/getProperty "java.io.tmpdir"))
;; That's how we find a file given a key.
;; Keys must match the given pattern.
(defn file [^String key]
{:pre [(re-matches #"^[A-Za-z-]+$" key)]}
(java.io.File. tmpdir key))
;; Here we handle GET requests. We just
;; read from a file.
(defn get-handler
[{:keys [params]}]
{:body (str (slurp (file (params :key))) "n")})
;; This is our PUT request handler. Given
;; a key and a value we write to a file.
(defn put-handler
[{:keys [params]}]
(let [val (params :val)]
(spit (file (params :key)) val)
{:body (str val "n") , :status 201}))
;; Here's the routing tree of our application.
;; We pick the handler depending on the HTTP
;; verb. On top of that we add an extra middle-
;; ware to parse data sent in requests.
(def handler
(-> ["/kv/" {[:key] {:get #'get-handler
:put #'put-handler}}]
(make-handler)
(wrap-defaults api-defaults)))
;; Finally, we've got all we need to expose
;; our handler over HTTP.
(defn -main []
(http/run-server handler
{:port 8080})
(println "🔥 http://localhost:8080"))
Docker images
FROM ubuntu AS BASE
RUN apt-get update
RUN apt-get install -yy curl leiningen build-essential zlib1g-dev
RUN cd /opt && curl -sL https://github.com/.../graalvm.tar.gz 
| tar -xzf -
ADD project.clj .
RUN lein deps
ADD src src
RUN lein uberjar
RUN /opt/graalvm-ce-1.0.0-rc5/bin/native-image 
-H:EnableURLProtocols=http --static --no-server 
-cp target/webkv-0.0.0-standalone.jar webkv.main
FROM scratch
COPY --from=BASE /webkv.main /
CMD ["/webkv.main"]
13
M
B
How Is That Even Possible
Time Memory
JVM 1.10 s 100 MB
JS 0.60 s 130 MB
Native 0.01 s 12 MB
static {
...
}
initialiser
Static
class
https://blog.ndk.io/clojure-compilation
GraalVM: Fast,
Polyglot, Native
Jan Stępień
@janstepien
janstepien.com
21.11.2018 // CODEMOTION BERLIN
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
https://creativecommons.org/licenses/by-nc-sa/3.0/

Jan Stępień - GraalVM: Fast, Polyglot, Native - Codemotion Berlin 2018

  • 1.
    GraalVM: Fast, Polyglot, Native 21.11.2018// CODEMOTION BERLIN Jan Stępień @janstepien janstepien.com
  • 2.
    JAN STĘPIEŃ Senior Consultant jan.stepien@innoq.com FunctionalProgramming Property-Based Testing Architecture and Development
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
    JEP 243 JVM CompilerInterface JD K 9
  • 8.
  • 9.
  • 10.
    Three main targetgroups JVM engineers Authors of languages Programmers and end users
  • 11.
  • 12.
  • 13.
    Three main targetgroups JVM engineers Authors of languages Programmers and end users
  • 14.
    Polyglot VM andtooling Faster execution Native compilation Programmers and end users
  • 15.
  • 17.
    public class HelloWorld{ public static void main(String[] args) { System.out.println("👋 Berlin"); } }
  • 18.
    $ time javaHelloWorld 👋 Berlin 0.14 real 0.13 user 0.03 sys 29M maximum resident set size
  • 19.
    $ native-image HelloWorld classlist:3,196.20 ms ⋮ image: 1,693.59 ms write: 472.40 ms [total]: 44,197.16 ms
  • 20.
    $ time ./HelloWorld 👋Berlin 0.00 real 0.00 user 0.00 sys 4M maximum resident set size
  • 22.
    Ahead-of-time compilation Just-in-time compilation Slower startup Higher topspeed High memory usage Faster startup Lower top speed Low memory usage
  • 23.
  • 24.
    $ docker inspectubuntu | jq .[0].Size
  • 25.
    (ns pprintin.main (:require [clojure.pprint:refer [pprint]]) (:gen-class)) (defn -main [& path] (-> (read *in*) (get-in (mapv read-string path)) pprint))
  • 26.
    $ echo {:a[5 3]} | time java -jar pprint.jar :a 1 3 1.51 real 112MB maximum resident set size
  • 27.
    $ echo {:a[5 3]} | java -XX:TieredStopAtLevel=1 -jar pprint.jar :a 1 3 1.06 real 96MB maximum resident set size
  • 28.
    $ echo {:a[5 3]} | time lumo pprint.cljs :a 1 3 0.56 real 128MB maximum resident set size
  • 29.
    $ native-image --no-server --static -jar pprint.jar classlist: 3,596.11 ms (...) image: 4,671.96 ms write: 14,763.97 ms [total]: 122,614.21 ms
  • 30.
    $ echo {:a[5 3]} | time ./pprint :a 1 3 0.01 real 12MB maximum resident set size
  • 31.
    Time Memory JVM 1.10 s100 MB JS 0.60 s 130 MB Native 0.01 s 12 MB
  • 32.
  • 33.
    (ns json2edn.core (:require [clojure.data.json:as json])) (defn -main [] (->> (repeatedly #(json/read *in* :key-fn keyword :eof-error? false :eof-value ::eof)) (take-while #(not= ::eof %)) (run! prn))) ;; See also Taylor Wood’s piece at ;; blog.taylorwood.io/2018/05/02/graal-clojure
  • 34.
    $ cat package.json| time json2edn {:description "Check if a value is an object", :engines {:node ">=0.10.0"}, :license "MIT", :repository "sindresorhus/is-obj", :name "is-obj", :scripts {:test "xo && ava"}, :keywords ["obj" "object" "is" "check" "test" "type"], :author {:name "Sindre Sorhus", :email "sindresorhus@gmail.com", :url "sindresorhus.com"}, :files ["index.js"], :version “1.0.1"} 0.01 real 10MB maximum resident set size
  • 36.
    (require '[spec-provider.provider :assp]) (def inferred-specs (sp/infer-specs [{:a 8 :b "foo" :c :k} {:a 10 :b "bar" :c "k"} {:a 1 :b "baz" :c "k"}] :toy/small-map)) (sp/pprint-specs inferred-specs 'toy ’s) ;; Results in the following being printed (s/def ::c (s/or :keyword keyword? :string string?)) (s/def ::b string?) (s/def ::a integer?) (s/def ::small-map (s/keys :req-un [::a ::b ::c]))
  • 37.
    (ns spec-provider.main (:require [spec-provider.provider:as sp] [clojure.string :as str] [clojure.edn :as edn]) (:gen-class)) (defn -main [spec-name] (let [kw (as-keyword spec-name) read-one (edn/read {:eof ::eof} *in*) samples (sequence (take-while (complement #{::eof})) (repeatedly read-one)) specs (sp/infer-specs samples kw)] (sp/pprint-specs specs (namespace kw) 's))))
  • 39.
    $ cat node_modules/*/package.json | json2edn | spec-provider :node/package | zprint ;; ... (s/def ::repository (s/or :map (s/keys :req-un [::type ::url]) :simple string?)) (s/def ::npm string?) (s/def ::node string?) (s/def ::engines (s/keys :req-un [::node] :opt-un [::iojs ::npm])) (s/def ::description string?) (s/def ::package (s/keys :req-un [::description ::name ::repository ::version] :opt-un [::author ::bin ::browser ::bugs ::contributors ::dependencies ::devDependencies ::engines ::files ::homepage ::icon ::keywords ::license ::main ::maintainers ::optionalDependencies ::peerDependencies ::scripts])) 0.29 real 103MB maximum resident set size
  • 40.
  • 41.
    $ curl -ihttp://localhost:8080/kv/city -X PUT -d val=Berlin HTTP/1.1 201 Created Content-Type: application/octet-stream Content-Length: 8 Server: http-kit Date: Mon, 03 Sep 2018 16:04:22 GMT Berlin $ curl -i http://localhost:8080/kv/city HTTP/1.1 200 OK Content-Type: application/octet-stream Content-Length: 8 Server: http-kit Date: Mon, 03 Sep 2018 16:04:27 GMT Berlin
  • 42.
    (ns webkv.main (:require [org.httpkit.server:as http] [ring.middleware.defaults :refer [wrap-defaults api-defaults]] [bidi.ring :refer [make-handler]]) (:gen-class)) ;; We want to be sure none of our calls relies ;; on reflection. Graal does not support them. (set! *warn-on-reflection* 1) ;; This is where we store our data. (def ^String tmpdir (System/getProperty "java.io.tmpdir")) ;; That's how we find a file given a key. ;; Keys must match the given pattern. (defn file [^String key] {:pre [(re-matches #"^[A-Za-z-]+$" key)]} (java.io.File. tmpdir key)) ;; Here we handle GET requests. We just ;; read from a file. (defn get-handler [{:keys [params]}] {:body (str (slurp (file (params :key))) "n")}) ;; This is our PUT request handler. Given ;; a key and a value we write to a file. (defn put-handler [{:keys [params]}] (let [val (params :val)] (spit (file (params :key)) val) {:body (str val "n") , :status 201})) ;; Here's the routing tree of our application. ;; We pick the handler depending on the HTTP ;; verb. On top of that we add an extra middle- ;; ware to parse data sent in requests. (def handler (-> ["/kv/" {[:key] {:get #'get-handler :put #'put-handler}}] (make-handler) (wrap-defaults api-defaults))) ;; Finally, we've got all we need to expose ;; our handler over HTTP. (defn -main [] (http/run-server handler {:port 8080}) (println "🔥 http://localhost:8080"))
  • 43.
  • 44.
    FROM ubuntu ASBASE RUN apt-get update RUN apt-get install -yy curl leiningen build-essential zlib1g-dev RUN cd /opt && curl -sL https://github.com/.../graalvm.tar.gz | tar -xzf - ADD project.clj . RUN lein deps ADD src src RUN lein uberjar RUN /opt/graalvm-ce-1.0.0-rc5/bin/native-image -H:EnableURLProtocols=http --static --no-server -cp target/webkv-0.0.0-standalone.jar webkv.main FROM scratch COPY --from=BASE /webkv.main / CMD ["/webkv.main"] 13 M B
  • 45.
    How Is ThatEven Possible
  • 46.
    Time Memory JVM 1.10 s100 MB JS 0.60 s 130 MB Native 0.01 s 12 MB
  • 48.
  • 49.
  • 51.
    GraalVM: Fast, Polyglot, Native JanStępień @janstepien janstepien.com 21.11.2018 // CODEMOTION BERLIN
  • 52.
    This work islicensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. https://creativecommons.org/licenses/by-nc-sa/3.0/