summaryrefslogtreecommitdiff
path: root/bench
diff options
context:
space:
mode:
authorAsko Nõmm <asko@nmm.ee>2026-02-17 20:29:53 +0200
committerAsko Nõmm <asko@nmm.ee>2026-02-17 20:29:53 +0200
commitd3058cd7e742771d97ec81c9e4ae1e96f954d4a4 (patch)
treef13ed9f447ee3b3bb62074d5e8d95bf9bf56ebfa /bench
parent1e2a95e4dab2c7b82c168a6a0fdce7d7485b4a8c (diff)
2.0: Improve performance, usability.
This is most likely a breaking change. Though from the API nothing changes, behaviour does. It will no longer match routes based on the first match, but rather the best match (the most specific route wins).
Diffstat (limited to 'bench')
-rw-r--r--bench/ruuter/bench.cljc134
1 files changed, 134 insertions, 0 deletions
diff --git a/bench/ruuter/bench.cljc b/bench/ruuter/bench.cljc
new file mode 100644
index 0000000..0048d13
--- /dev/null
+++ b/bench/ruuter/bench.cljc
@@ -0,0 +1,134 @@
+(ns ruuter.bench
+ (:require [ruuter.core :as ruuter]))
+
+;; ── Benchmark Harness ──────────────────────────────────────────────────
+;; Cross-platform (JVM, ClojureScript/Node.js, Babashka).
+
+(defn- now-ns
+ "Returns the current time in nanoseconds."
+ []
+ #?(:cljs (* (.now js/performance) 1e6)
+ :clj (System/nanoTime)))
+
+(defn- bench
+ "Run `f` for `warmup-ms` to warm up, then run it for `measure-ms` and
+ return {:ops n, :elapsed-ms ms, :ops-per-sec ops/s, :ns-per-op ns}."
+ [f & {:keys [warmup-ms measure-ms] :or {warmup-ms 2000 measure-ms 5000}}]
+ ;; warmup
+ (let [warmup-end (+ (now-ns) (* warmup-ms 1e6))]
+ (while (< (now-ns) warmup-end) (f)))
+ ;; measure
+ (let [start (now-ns)
+ deadline (+ start (* measure-ms 1e6))
+ total-ops (loop [cnt 0]
+ (f)
+ (let [cnt' (inc cnt)]
+ (if (< (now-ns) deadline)
+ (recur cnt')
+ cnt')))
+ elapsed-ns (- (now-ns) start)
+ elapsed-ms (/ elapsed-ns 1e6)
+ ops-per-sec (long (/ (* total-ops 1e9) elapsed-ns))
+ ns-per-op (long (/ elapsed-ns total-ops))]
+ {:ops total-ops
+ :elapsed-ms (long elapsed-ms)
+ :ops-per-sec ops-per-sec
+ :ns-per-op ns-per-op}))
+
+(defn- format-number
+ "Formats an integer with comma grouping, cross-platform."
+ [n]
+ #?(:cljs (let [s (str n)
+ reversed-digits (reverse s)
+ groups (map #(apply str (reverse %))
+ (partition-all 3 reversed-digits))]
+ (string/join "," (reverse groups)))
+ :clj (format "%,d" n)))
+
+(defn- format-result [label {:keys [ops-per-sec ns-per-op]}]
+ (let [ops-str (format-number ops-per-sec)
+ ns-str (format-number ns-per-op)]
+ (str "| " label
+ (apply str (repeat (max 1 (- 46 (count label))) " "))
+ "| " (apply str (repeat (max 0 (- 16 (count ops-str))) " ")) ops-str
+ " | " (apply str (repeat (max 0 (- 13 (count ns-str))) " ")) ns-str
+ " |")))
+
+;; ── Route Definitions ──────────────────────────────────────────────────
+
+(def small-routes
+ "5 routes — typical small app."
+ [{:path "/" :method :get
+ :response {:status 200 :body "home"}}
+ {:path "/about" :method :get
+ :response {:status 200 :body "about"}}
+ {:path "/users/:id" :method :get
+ :response (fn [req] {:status 200 :body (str "user " (:id (:params req)))})}
+ {:path "/users/:id/posts/:post-id" :method :get
+ :response (fn [_req] {:status 200 :body "post"})}
+ {:path "/files/:path*" :method :get
+ :response (fn [_req] {:status 200 :body "file"})}])
+
+(defn- generate-routes
+ "Generate `n` routes of the form /prefix-N/:id."
+ [n]
+ (vec
+ (concat
+ [{:path "/" :method :get :response {:status 200 :body "home"}}]
+ (for [i (range n)]
+ {:path (str "/section-" i "/:id")
+ :method :get
+ :response (fn [_req] {:status 200 :body (str "section " i)})})
+ [{:path "/:catch-all*" :method :get
+ :response (fn [_req] {:status 200 :body "catch all"})}])))
+
+(def medium-routes (generate-routes 50))
+(def large-routes (generate-routes 200))
+
+;; ── Benchmark Scenarios ────────────────────────────────────────────────
+
+(defn run-benchmarks []
+ (println "\nRunning benchmarks (2s warmup + 5s measure each)...\n")
+
+ (let [results
+ [["Small (5) — literal first"
+ (bench #(ruuter/route small-routes {:uri "/" :request-method :get}))]
+ ["Small (5) — literal middle"
+ (bench #(ruuter/route small-routes {:uri "/about" :request-method :get}))]
+ ["Small (5) — param match"
+ (bench #(ruuter/route small-routes {:uri "/users/42" :request-method :get}))]
+ ["Small (5) — nested params"
+ (bench #(ruuter/route small-routes {:uri "/users/42/posts/7" :request-method :get}))]
+ ["Small (5) — wildcard"
+ (bench #(ruuter/route small-routes {:uri "/files/a/b/c/d.txt" :request-method :get}))]
+ ["Small (5) — miss (404)"
+ (bench #(ruuter/route small-routes {:uri "/nope" :request-method :get}))]
+ ["Medium (52) — match first"
+ (bench #(ruuter/route medium-routes {:uri "/" :request-method :get}))]
+ ["Medium (52) — match middle"
+ (bench #(ruuter/route medium-routes {:uri "/section-25/42" :request-method :get}))]
+ ["Medium (52) — match last"
+ (bench #(ruuter/route medium-routes {:uri "/section-49/42" :request-method :get}))]
+ ["Medium (52) — catch-all wildcard"
+ (bench #(ruuter/route medium-routes {:uri "/unknown/path" :request-method :get}))]
+ ["Large (202) — match first"
+ (bench #(ruuter/route large-routes {:uri "/" :request-method :get}))]
+ ["Large (202) — match middle"
+ (bench #(ruuter/route large-routes {:uri "/section-100/42" :request-method :get}))]
+ ["Large (202) — match last"
+ (bench #(ruuter/route large-routes {:uri "/section-199/42" :request-method :get}))]
+ ["Large (202) — miss (404)"
+ (bench #(ruuter/route large-routes {:uri "/nothing" :request-method :post}))]]]
+
+ (println "| Scenario | Ops/sec | ns/op |")
+ (println "|-----------------------------------------------|-----------------|--------------|")
+ (doseq [[label result] results]
+ (println (format-result label result)))
+ (println)
+
+ results))
+
+(defn -main []
+ (run-benchmarks))
+
+#?(:cljs (set! *main-cli-fn* -main))