blob: 0048d134c02b252a4a078fd824f4348093e39d98 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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))
|