(ns ruuter.bench (:require [ruuter.core :as ruuter])) ;; -- Benchmark Harness --------------------------------------------------- ;; Cross-platform (JVM, ClojureScript/Node.js, Babashka, Jank). #?(:jank (cpp/raw "#include int64_t now_ns() { return std::chrono::duration_cast( 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/now_ns) :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))