diff options
| author | Asko Nõmm <asko@nmm.ee> | 2026-02-17 20:29:53 +0200 |
|---|---|---|
| committer | Asko Nõmm <asko@nmm.ee> | 2026-02-17 20:29:53 +0200 |
| commit | d3058cd7e742771d97ec81c9e4ae1e96f954d4a4 (patch) | |
| tree | f13ed9f447ee3b3bb62074d5e8d95bf9bf56ebfa /bench | |
| parent | 1e2a95e4dab2c7b82c168a6a0fdce7d7485b4a8c (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.cljc | 134 |
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)) |
