summaryrefslogtreecommitdiff
path: root/bench/ruuter/bench.cljc
diff options
context:
space:
mode:
Diffstat (limited to 'bench/ruuter/bench.cljc')
-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))