The original code looks like this:
(ns hello.handler
(:require
[byte-streams :as bs]
[clojure.tools.cli :as cli]
[aleph.http :as http]
[cheshire.core :as json]
[clj-tuple :as t])
(:gen-class))
(def plaintext-response
(t/hash-map
:status 200
:headers (t/hash-map "content-type" "text/plain; charset=utf-8")
:body (bs/to-byte-array "Hello, World!")))
(def json-response
(t/hash-map
:status 200
:headers (t/hash-map "content-type" "application/json")))
(defn handler [{:keys [uri] :as req}]
(cond
(= "/plaintext" uri) plaintext-response
(= "/json" uri) (assoc json-response
:body (json/encode (t/hash-map :message "Hello, World!")))
:else {:status 404}))
;;;
(defn -main [& args]
(let [[{:keys [help port]} _ banner]
(cli/cli args
["-p" "--port" "Server port"
:default 8080
:parse-fn #(Integer/parseInt %)]
["-h" "--[no-]help"])]
(when help
(println banner)
(System/exit 0))
(aleph.netty/leak-detector-level! :disabled)
(http/start-server handler {:port port, :executor :none})))
With my laptop:
;; start repl with `lein perf repl`
;; perf measured with the following setup:
;;
;; Model Name: MacBook Pro
;; Model Identifier: MacBookPro11,3
;; Processor Name: Intel Core i7
;; Processor Speed: 2,5 GHz
;; Number of Processors: 1
;; Total Number of Cores: 4
;; L2 Cache (per Core): 256 KB
;; L3 Cache: 6 MB
;; Memory: 16 GB
using
wrk -t2 -c100 -d2s http://localhost:8080/json
serves about 82700 req/sec.
Benchmaring with JVM-opts with Criterium and with Aleph 0.4.4
:
["-server"
"-Xmx4096m"
"-Dclojure.compiler.direct-linking=true"]
Setup:
(require '[byte-streams :as bs]
'[aleph.http :as http]
'[cheshire.core :as cheshire]
'[clj-tuple :as t])
(def plaintext-response
(t/hash-map
:status 200
:headers (t/hash-map "content-type" "text/plain; charset=utf-8")
:body (bs/to-byte-array "Hello, World!")))
(def json-response
(t/hash-map
:status 200
:headers (t/hash-map "content-type" "application/json")))
we need to capture a real Aleph request:
(class +request+)
; aleph.http.core.NettyRequest
+request+
;{:aleph/request-arrived 9336693625230,
; :aleph/keep-alive? true,
; :remote-addr nil,
; :headers {"host" "localhost:8080",
; "user-agent" "HTTPie/0.9.9",
; "connection" "keep-alive",
; "accept" "*/*",
; "accept-encoding" "gzip, deflate"},
; :server-port 8080,
; :uri "/json",
; :server-name "0.0.0.0",
; :query-string nil,
; :body nil,
; :scheme :http,
; :request-method :get}
(require '[criterium.core :as cc])
(defn handler1 [{:keys [uri] :as req}]
(cond
(= "/plaintext" uri) plaintext-response
(= "/json" uri) (assoc json-response
:body (cheshire/encode (t/hash-map :message "Hello, World!")))
:else {:status 404}))
(handler1 +request+)
; => {:status 200, :headers {"content-type" "application/json"}, :body "{\"message\":\"Hello, World!\"}"}
;; 1.40µs
(cc/quick-bench (handler1 +request+))
https://github.com/metosin/jsonista is Clojure library for fast JSON encoding and decoding.
(require '[jsonista.core :as jsonista])
(defn handler2 [{:keys [uri] :as req}]
(cond
(= "/plaintext" uri) plaintext-response
(= "/json" uri) (assoc json-response
:body (jsonista/write-value-as-bytes (t/hash-map :message "Hello, World!")))
:else {:status 404}))
(handler2 +request+)
; => {:status 200, :headers {"content-type" "application/json"}, :body #object["[B" 0x78d5200b "[B@78d5200b"]}
;; 371ns
(cc/quick-bench (handler2 +request+))
-74%, nice. But let's not stop here.
Destructuring Maps is slow in Clojure. I have a patch bubblin' for it, but let's destrucure manually here to see the difference:
(defn handler3 [req]
(let [uri (:uri req)]
(cond
(= "/plaintext" uri) plaintext-response
(= "/json" uri) (assoc json-response
:body (jsonista/write-value-as-bytes (t/hash-map :message "Hello, World!")))
:else {:status 404})))
(handler3 +request+)
; {:status 200, :headers {"content-type" "application/json"}, :body #object["[B" 0x2e58e486 "[B@2e58e486"]}
;; 344ns
(cc/quick-bench (handler3 +request+))
-8%. Easy win.
=
is over 100x slower than .equals
for Strings - which is backed with a JVM optimization. Let's see if that counts:
(defn handler4 [req]
(let [uri (:uri req)]
(cond
(.equals "/plaintext" uri) plaintext-response
(.equals "/json" uri) (assoc json-response
:body (jsonista/write-value-as-bytes (t/hash-map :message "Hello, World!")))
:else {:status 404})))
(handler4 +request+)
; {:status 200, :headers {"content-type" "application/json"}, :body #object["[B" 0x2b98ab27 "[B@2b98ab27"]}
;; 269ns
(cc/quick-bench (handler4 +request+))
-21% less, nice!
What if we used plain Clojure Maps instead? would be more like the real code we do. Let's try:
(defn handler5 [req]
(let [uri (:uri req)]
(cond
(.equals "/plaintext" uri) {:status 200
:headers {"content-type" "text/plain; charset=utf-8"}
:body (bs/to-byte-array "Hello, World!")}
(.equals "/json" uri) {:status 200
:headers {"content-type" "application/json"}
:body (jsonista/write-value-as-bytes {:message "Hello, World!"})}
:else {:status 404})))
(handler5 +request+)
; {:status 200, :headers {"content-type" "application/json"}, :body #object["[B" 0x798ba1b2 "[B@798ba1b2"]}
;; 265ns
(cc/quick-bench (handler5 +request+))
Seems not to make no difference. But it's 80% faster than the original code.
With the last code, the original benchmark:
wrk -t2 -c100 -d2s http://localhost:8080/json
serves about 84500 req/sec, which is +2%.
not much, but could be better on a real test setup.
from:
java -server -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=10 -jar target/hello-aleph-standalone.jar
to:
java -server -XX:+UseNUMA -XX:+UseParallelGC -XX:+AggressiveOpts -jar target/hello-aleph-standalone.jar
serves about 87000 req/sec, which is +3% more.
(not actual idea how different jvm parameters effect, but this seems better...)
Requests/sec: 82651.72
Transfer/sec: 14.19MB
Requests/sec: 82110.08
Transfer/sec: 14.10MB
Requests/sec: 84551.86
Transfer/sec: 14.51MB
Requests/sec: 86997.37
Transfer/sec: 14.93MB
The whole inlined response:
will generate java code ~like this:
... where the
RT.mapUniqueKeys
looks like:... while the assoc-version:
would ~produce: