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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
(ns ruuter.bench
(:require [ruuter.core :as ruuter]))
;; -- Benchmark Harness ---------------------------------------------------
;; Cross-platform (JVM, ClojureScript/Node.js, Babashka, Jank).
#?(:jank
(cpp/raw "#include <chrono>
int64_t ruuter_bench_clock_now_ns_cpp()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count();
}
"))
(defn- now-ns
"Returns the current time in nanoseconds."
[]
#?(:cljs (* (.now js/performance) 1e6)
:jank (cpp/ruuter_bench_clock_now_ns_cpp)
:default (System/nanoTime)))
(defn- ->long
"Coerces a number to a long integer, cross-platform.
Uses `int` on Jank since `long` is not yet implemented."
[n]
#?(:jank (int n)
:default (long n)))
(defn- ->double
"Coerces a number to a double, cross-platform."
[n]
(double n))
(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) (->long (* warmup-ms 1e6)))]
(while (< (now-ns) warmup-end) (f)))
;; measure
(let [start (now-ns)
deadline (+ start (->long (* 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 (/ (->double elapsed-ns) 1e6)
ops-per-sec (->long (/ (* (->double total-ops) 1e9) (->double elapsed-ns)))
ns-per-op (->long (/ (->double elapsed-ns) (->double 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]
#?(:clj (format "%,d" n)
:default (let [s (str n)
reversed-digits (reverse s)
groups (map #(apply str (reverse %))
(partition-all 3 reversed-digits))]
(apply str (interpose "," (reverse groups))))))
(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))
|