Browse Source

Clojure: Add Kit framework (#7826)

* initial pass at kit clojure TE benchmark implementation

* update readme

* update gitignore to match guidelines of PR

* remove more unused parts, to comply with pr requirements

* more removal unused elements

* deps tweaks, add back log config

* tweak readme
Nik Peric 2 years ago
parent
commit
f285a7fc9b

+ 23 - 0
frameworks/Clojure/kit/.gitignore

@@ -0,0 +1,23 @@
+kit.git-config.edn
+.DS_Store
+.nrepl-port
+hs_err_pid*.log
+.cpcache/
+target
+log/
+.shadow-cljs/
+node_modules/
+modules/
+
+# IntelliJ
+*.iml
+.idea/
+
+# clj-kondo
+.clj-kondo/.cache/
+.calva/output-window/
+.lsp/.cache/
+
+# local dev files that TE benchmark doesn't want
+docker-compose.yml
+env/dev/

+ 53 - 0
frameworks/Clojure/kit/README.md

@@ -0,0 +1,53 @@
+# Kit Benchmarking Test
+
+This is an implementation using the [Kit web framework](https://kit-clj.github.io/).
+
+It uses PostgreSQL with [HikariCP](https://github.com/tomekw/hikari-cp)
+and [next.jdbc](https://github.com/seancorfield/next-jdbc), [Selmer](https://github.com/yogthos/Selmer) for HTTP
+templating, [Muuntaja](https://github.com/metosin/muuntaja) (with [jsonista](https://github.com/metosin/jsonista))
+for content type coercion, [Undertow](https://github.com/luminus-framework/ring-undertow-adapter) for the web
+server, [ring](https://github.com/ring-clojure/ring) and [reitit](https://github.com/metosin/reitit) for HTTP
+abstraction and routing.
+
+### Test Type Implementation Source Code
+
+* [JSON](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L85)
+* [PLAINTEXT](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L89)
+* [DB](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L94)
+* [QUERY](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L98)
+* [CACHED QUERY](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L102)
+* [UPDATE](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L111)
+* [FORTUNES](src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj#L125)
+
+Requirements
+URL: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#general-test-requirements
+
+## Test URLs
+
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/query?queries=
+
+### CACHED QUERY
+
+http://localhost:8080/cached-queries?queries=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 31 - 0
frameworks/Clojure/kit/benchmark_config.json

@@ -0,0 +1,31 @@
+{
+  "framework": "kit",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "cached_query_url": "/cached-queries?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Kit",
+        "language": "Clojure",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "Undertow",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Kit",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 41 - 0
frameworks/Clojure/kit/build.clj

@@ -0,0 +1,41 @@
+(ns build
+  (:require [clojure.string :as string]
+            [clojure.tools.build.api :as b]))
+
+(def lib 'io.github.kit-clj/te-bench)
+(def main-cls (string/join "." (filter some? [(namespace lib) (name lib) "core"])))
+(def version (format "0.0.1-SNAPSHOT"))
+(def target-dir "target")
+(def class-dir (str target-dir "/" "classes"))
+(def uber-file (format "%s/%s-standalone.jar" target-dir (name lib)))
+(def basis (b/create-basis {:project "deps.edn"}))
+
+(defn clean
+  "Delete the build target directory"
+  [_]
+  (println (str "Cleaning " target-dir))
+  (b/delete {:path target-dir}))
+
+(defn prep [_]
+  (println "Writing Pom...")
+  (b/write-pom {:class-dir class-dir
+                :lib lib
+                :version version
+                :basis basis
+                :src-dirs ["src/clj"]})
+  (b/copy-dir {:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
+               :target-dir class-dir}))
+
+(defn uber [_]
+  (println "Compiling Clojure...")
+  (b/compile-clj {:basis basis
+                  :src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
+                  :class-dir class-dir})
+  (println "Making uberjar...")
+  (b/uber {:class-dir class-dir
+           :uber-file uber-file
+           :main main-cls
+           :basis basis}))
+
+(defn all [_]
+  (do (clean nil) (prep nil) (uber nil)))

+ 46 - 0
frameworks/Clojure/kit/deps.edn

@@ -0,0 +1,46 @@
+{:paths   ["src/clj"
+           "resources"]
+
+ :deps    {org.clojure/clojure              {:mvn/version "1.11.1"}
+
+           ;; Routing
+           metosin/reitit                   {:mvn/version "0.5.18"}
+
+           ;; Ring
+           metosin/ring-http-response       {:mvn/version "0.9.3"}
+           ring/ring-core                   {:mvn/version "1.9.5"}
+
+           ;; Data coercion
+           metosin/muuntaja                 {:mvn/version "0.6.8"}
+
+           ;; HTML templating
+           selmer/selmer                    {:mvn/version "1.12.55"}
+
+           ;; Database
+           org.postgresql/postgresql        {:mvn/version "42.5.1"}
+
+           ;; kit Libs
+           io.github.kit-clj/kit-core       {:mvn/version "1.0.3"}
+           io.github.kit-clj/kit-undertow   {:mvn/version "1.0.4"}
+           io.github.kit-clj/kit-sql-hikari {:mvn/version "1.0.2"}
+           org.clojure/core.cache           {:mvn/version "1.0.225"}
+
+           }
+
+ :aliases {:build {:deps       {io.github.clojure/tools.build {:git/sha "e3e3532"
+                                                               :git/tag "v0.8.0" :git/url "https://github.com/clojure/tools.build.git"}}
+                   :ns-default build}
+
+           :dev   {:extra-deps  {com.lambdaisland/classpath      {:mvn/version "0.0.27"}
+                                 criterium/criterium             {:mvn/version "0.4.6"}
+                                 expound/expound                 {:mvn/version "0.9.0"}
+                                 integrant/repl                  {:mvn/version "0.3.2"}
+                                 pjstadig/humane-test-output     {:mvn/version "0.11.0"}
+                                 ring/ring-devel                 {:mvn/version "1.9.5"}
+                                 ring/ring-mock                  {:mvn/version "0.4.0"}
+                                 io.github.kit-clj/kit-generator {:mvn/version "0.1.7"}
+                                 org.clojure/tools.namespace     {:mvn/version "1.2.0"}
+                                 }
+                   :extra-paths ["env/dev/clj" "env/dev/resources" "test/clj"]}
+           }
+ }

+ 11 - 0
frameworks/Clojure/kit/env/prod/clj/io/github/kit_clj/te_bench/env.clj

@@ -0,0 +1,11 @@
+(ns io.github.kit-clj.te-bench.env
+  (:require [clojure.tools.logging :as log]))
+
+(def defaults
+  {:init       (fn []
+                 (log/info "\n-=[ starting]=-"))
+   :start      (fn []
+                 (log/info "\n-=[ started successfully]=-"))
+   :stop       (fn []
+                 (log/info "\n-=[ has shut down successfully]=-"))
+   :opts       {:profile :prod}})

+ 21 - 0
frameworks/Clojure/kit/env/prod/resources/logback.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="10 seconds">
+    <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- encoders are assigned the type
+             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
+        <encoder>
+            <charset>UTF-8</charset>
+            <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
+        </encoder>
+    </appender>
+    <logger name="org.eclipse.aether" level="warn" />
+    <logger name="io.methvin.watcher" level="warn" />
+    <logger name="org.eclipse.jgit" level="warn" />
+    <logger name="com.zaxxer.hikari" level="warn" />
+    <logger name="org.apache.http" level="warn" />
+    <logger name="org.xnio.nio" level="warn" />
+    <logger name="io.undertow" level="warn" />
+    <root level="INFO">
+    </root>
+</configuration>

+ 19 - 0
frameworks/Clojure/kit/kit.dockerfile

@@ -0,0 +1,19 @@
+# syntax = docker/dockerfile:1.2
+FROM clojure:openjdk-17 AS build
+
+WORKDIR /
+COPY . /
+
+RUN clj -Sforce -T:build all
+
+FROM azul/zulu-openjdk-alpine:17
+
+COPY --from=build /target/te-bench-standalone.jar /te-bench/te-bench-standalone.jar
+
+EXPOSE 8080
+
+ENV PORT=8080
+ENV JAVA_OPTS="-XX:+UseContainerSupport -Dfile.encoding=UTF-8"
+ENV JDBC_URL="jdbc:postgresql://tfb-database/hello_world?user=benchmarkdbuser&password=benchmarkdbpass"
+
+ENTRYPOINT exec java $JAVA_OPTS -jar /te-bench/te-bench-standalone.jar

+ 15 - 0
frameworks/Clojure/kit/resources/html/fortunes.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+    <tr><th>id</th><th>message</th></tr>
+    {% for message in messages %}
+    <tr>
+        <td>{{message.id}}</td>
+        <td>{{message.message}}</td>
+    </tr>
+    {% endfor %}
+</table>
+</body>
+</html>

+ 31 - 0
frameworks/Clojure/kit/resources/system.edn

@@ -0,0 +1,31 @@
+{:system/env
+ #profile {:dev  :dev
+           :test :test
+           :prod :prod}
+
+ :server/http
+ {:port    #long #or [#env PORT 8080]
+  :host    #or [#env HTTP_HOST "0.0.0.0"]
+  :handler #ig/ref :handler/ring}
+
+ :handler/ring
+ {:router #ig/ref :router/core}
+
+ :reitit.routes/bench
+ {:base-path ""
+  :db-conn   #ig/ref :db.sql/hikari-connection
+  :cache     #ig/ref :cache/inmem}
+
+ :router/routes
+ {:routes #ig/refset :reitit/routes}
+
+ :router/core
+ {:routes #ig/ref :router/routes}
+
+ :db.sql/hikari-connection
+ {:jdbc-url #or [#env JDBC_URL "jdbc:postgresql://localhost:5432/hello_world?user=benchmarkdbuser&password=benchmarkdbpass"]}
+
+ :cache/inmem
+ {:db-conn  #ig/ref :db.sql/hikari-connection
+  :threshold 10000}
+ }

+ 16 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/cache/inmem.clj

@@ -0,0 +1,16 @@
+(ns io.github.kit-clj.te-bench.cache.inmem
+  (:require
+    [clojure.core.cache :as cache]
+    [integrant.core :as ig]
+    [next.jdbc :as jdbc]
+    [next.jdbc.result-set :as rs]))
+
+(defmethod ig/init-key :cache/inmem
+  [_ {:keys [db-conn threshold]}]
+  (cache/fifo-cache-factory
+    (reduce
+      (fn [out {:keys [id] :as obj}]
+        (assoc out id obj))
+      {}
+      (jdbc/execute! db-conn ["select * from \"World\""] {:builder-fn rs/as-unqualified-lower-maps}))
+    {:threshold threshold}))

+ 9 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/config.clj

@@ -0,0 +1,9 @@
+(ns io.github.kit-clj.te-bench.config
+  (:require
+    [kit.config :as config]))
+
+(def ^:const system-filename "system.edn")
+
+(defn system-config
+  [options]
+  (config/read-config system-filename options))

+ 42 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/core.clj

@@ -0,0 +1,42 @@
+(ns io.github.kit-clj.te-bench.core
+  (:require
+    [clojure.tools.logging :as log]
+    [integrant.core :as ig]
+    [io.github.kit-clj.te-bench.config :as config]
+    [io.github.kit-clj.te-bench.env :refer [defaults]]
+
+    ;; Edges       
+    [io.github.kit-clj.te-bench.cache.inmem]
+    [io.github.kit-clj.te-bench.db.sql.hikari]
+    [io.github.kit-clj.te-bench.web.handler]
+    [kit.edge.server.undertow]
+
+    ;; Routes
+    [io.github.kit-clj.te-bench.web.routes.bench])
+  (:gen-class))
+
+;; log uncaught exceptions in threads
+(Thread/setDefaultUncaughtExceptionHandler
+  (reify Thread$UncaughtExceptionHandler
+    (uncaughtException [_ thread ex]
+      (log/error {:what :uncaught-exception
+                  :exception ex
+                  :where (str "Uncaught exception on" (.getName thread))}))))
+
+(defonce system (atom nil))
+
+(defn stop-app []
+  ((or (:stop defaults) (fn [])))
+  (some-> (deref system) (ig/halt!))
+  (shutdown-agents))
+
+(defn start-app [& [params]]
+  ((or (:start params) (:start defaults) (fn [])))
+  (->> (config/system-config (or (:opts params) (:opts defaults) {}))
+       (ig/prep)
+       (ig/init)
+       (reset! system))
+  (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)))
+
+(defn -main [& _]
+  (start-app))

+ 9 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/db/sql/hikari.clj

@@ -0,0 +1,9 @@
+(ns io.github.kit-clj.te-bench.db.sql.hikari
+  (:require
+    [integrant.core :as ig]
+    [kit.edge.db.sql.hikari]))
+
+(defmethod ig/prep-key :db.sql/hikari-connection
+  [_ config]
+  (let [cpus (.availableProcessors (Runtime/getRuntime))]
+    (assoc config :maximum-pool-size (* 8 cpus))))

+ 130 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/web/controllers/bench.clj

@@ -0,0 +1,130 @@
+(ns io.github.kit-clj.te-bench.web.controllers.bench
+  (:require
+    [clojure.core.cache :as cache]
+    [next.jdbc :as jdbc]
+    [next.jdbc.result-set :as rs]
+    [ring.util.http-response :as http-response]
+    [selmer.parser :as parser]))
+
+;; -----------
+;; Utils
+;; -----------
+
+(def ^:const HELLO_WORLD "Hello, World!")
+(def ^:const MAX_ID_ZERO_IDX 9999)
+(def ^:const CACHE_TTL (* 24 60 60))
+
+(def selmer-opts {:custom-resource-path (clojure.java.io/resource "html")})
+
+(defn html-response
+  [template & [params]]
+  (-> (parser/render-file template params selmer-opts)
+      (http-response/ok)
+      (http-response/content-type "text/html; charset=utf-8")))
+
+(defn rand-id
+  [n]
+  (inc (rand-int n)))
+
+;; From Luminus benchmark
+(defn query-count
+  "Parse provided string value of query count, clamping values to between 1 and 500."
+  [^String queries]
+  (let [n (try (Integer/parseInt queries)
+               (catch Exception _ 1))]                ; default to 1 on parse failure
+    (cond
+      (< n 1) 1
+      (> n 500) 500
+      :else n)))
+
+(defn range-from-req
+  [request]
+  (range (query-count (get-in request [:query-params "queries"] "1"))))
+
+;; -----------
+;; Queries
+;; -----------
+
+(def jdbc-opts {:builder-fn rs/as-unqualified-lower-maps})
+
+(defn db-query-world!
+  [db-conn]
+  (jdbc/execute-one! db-conn ["select * from \"World\" where id = ?" (rand-id MAX_ID_ZERO_IDX)]
+                     jdbc-opts))
+
+(defn db-multi-query-world!
+  "Queries multiple times outside context of a tx"
+  [db-conn request]
+  (reduce
+    (fn [out _]
+      (conj out (db-query-world! db-conn)))
+    []
+    (range-from-req request)))
+
+(defn update-world!
+  [db-conn id rand-number]
+  (jdbc/execute-one! db-conn
+                     ["update \"World\" set randomNumber = ? where id = ? returning *;" rand-number id]
+                     jdbc-opts))
+
+;; -----------
+;; Cache
+;; -----------
+
+(defn cache-lookup-or-add
+  [cache key lookup-fn ttl]
+  (or (cache/lookup cache key)
+      (let [value (lookup-fn)]
+        (cache/miss cache key {:val value :ttl ttl})
+        value)))
+
+;; -----------
+;; Handlers
+;; -----------
+
+(defn json-handler
+  [_]
+  (http-response/ok {:message HELLO_WORLD}))
+
+(defn plaintext-handler
+  [_]
+  (-> (http-response/ok HELLO_WORLD)
+      (http-response/content-type "text/plain")))
+
+(defn db-handler
+  [db-conn _request]
+  (http-response/ok (db-query-world! db-conn)))
+
+(defn multi-db-handler
+  [db-conn request]
+  (http-response/ok (db-multi-query-world! db-conn request)))
+
+(defn update-db-handler
+  [db-conn request]
+  (let [items   (db-multi-query-world! db-conn request)]
+    (http-response/ok
+      (mapv
+        (fn [{:keys [id]}]
+          (update-world! db-conn id (rand-id MAX_ID_ZERO_IDX)))
+        items))))
+
+(defn cached-query-handler
+  [db-conn cache request]
+  (http-response/ok
+    (reduce
+      (fn [out _]
+        (let [id (rand-id MAX_ID_ZERO_IDX)]
+          (conj out
+                (cache-lookup-or-add cache
+                                     id
+                                     #(jdbc/execute-one! db-conn ["select * from \"World\" where id = ?;" id] jdbc-opts)
+                                     CACHE_TTL))))
+      []
+      (range-from-req request))))
+
+(defn fortune-handler
+  [db-conn _request]
+  (as-> (jdbc/execute! db-conn ["select * from \"Fortune\";"] jdbc-opts) fortunes
+        (conj fortunes {:id 0 :message "Additional fortune added at request time."})
+        (sort-by :message fortunes)
+        (html-response "fortunes.html" {:messages fortunes})))

+ 21 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/web/handler.clj

@@ -0,0 +1,21 @@
+(ns io.github.kit-clj.te-bench.web.handler
+  (:require
+    [integrant.core :as ig]
+    [reitit.ring :as ring]
+    ))
+
+(defmethod ig/init-key :handler/ring
+  [_ {:keys [router]}]
+  (ring/ring-handler
+    router
+    nil
+    {:inject-match?  false
+     :inject-router? false}))
+
+(defmethod ig/init-key :router/routes
+  [_ {:keys [routes]}]
+  (apply conj [] routes))
+
+(defmethod ig/init-key :router/core
+  [_ {:keys [routes] :as opts}]
+  (ring/router ["" opts routes]))

+ 14 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/web/middleware/default_headers.clj

@@ -0,0 +1,14 @@
+(ns io.github.kit-clj.te-bench.web.middleware.default-headers
+  (:require
+    [ring.util.http-response :as http-response]))
+
+(def default-headers-middleware
+  "Adds default headers required for TechEmpower benchmarks"
+  {:name    ::default-headers
+   :compile (fn [_route-data _opts]
+              (fn [handler]
+                (fn
+                  ([request]
+                   (http-response/header (handler request) "Server" "Kit"))
+                  ([request respond raise]
+                   (handler request #(respond (http-response/header % "Server" "Kit")) raise)))))})

+ 8 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/web/middleware/formats.clj

@@ -0,0 +1,8 @@
+(ns io.github.kit-clj.te-bench.web.middleware.formats
+  (:require
+    [muuntaja.core :as m]
+    [muuntaja.format.json :as json-format]))
+
+(def instance
+  (m/create (assoc m/default-options
+              :formats {"application/json" json-format/format})))

+ 34 - 0
frameworks/Clojure/kit/src/clj/io/github/kit_clj/te_bench/web/routes/bench.clj

@@ -0,0 +1,34 @@
+(ns io.github.kit-clj.te-bench.web.routes.bench
+  (:require
+    [io.github.kit-clj.te-bench.web.controllers.bench :as bench]
+    [io.github.kit-clj.te-bench.web.middleware.default-headers :as default-headers]
+    [io.github.kit-clj.te-bench.web.middleware.formats :as formats]
+    [integrant.core :as ig]
+    [reitit.ring.middleware.muuntaja :as muuntaja]
+    [reitit.ring.middleware.parameters :as parameters]))
+
+(derive :reitit.routes/bench :reitit/routes)
+
+;; Routes
+(defn bench-routes [{:keys [db-conn cache]}]
+  [["/json" {:get bench/json-handler}]
+   ["/plaintext" {:get bench/plaintext-handler}]
+   ["/db" {:get (partial bench/db-handler db-conn)}]
+   ["/queries" {:get (partial bench/multi-db-handler db-conn)}]
+   ["/updates" {:get (partial bench/update-db-handler db-conn)}]
+   ["/cached-queries" {:get (partial bench/cached-query-handler db-conn cache)}]
+   ["/fortunes" {:get (partial bench/fortune-handler db-conn)}]])
+
+(defmethod ig/init-key :reitit.routes/bench
+  [_ {:keys [base-path]
+      :or   {base-path ""}
+      :as   opts}]
+  [base-path
+   {:muuntaja   formats/instance
+    :middleware [;; query-params & form-params
+                 parameters/parameters-middleware
+                 ;; encoding response body
+                 muuntaja/format-response-middleware
+                 ;; default header middleware
+                 default-headers/default-headers-middleware]}
+   (bench-routes opts)])