diff --git a/doc/advanced/different_routers.md b/doc/advanced/different_routers.md index ae742760..98e01654 100644 --- a/doc/advanced/different_routers.md +++ b/doc/advanced/different_routers.md @@ -1,18 +1,25 @@ # Different Routers -Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](advanced/configuring_routers.md). +Reitit ships with several different implementations for the `Router` protocol, originally based on the [Pedestal](https://github.com/pedestal/pedestal/tree/master/route) implementation. `router` function selects the most suitable implementation by inspecting the expanded routes. The implementation can be set manually using `:router` option, see [configuring routers](advanced/configuring_routers.md). | router | description | | ------------------------------|-------------| -| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. -| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters. -| `:mixed-router` | Creates internally a `:linear-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid if there are no [Route conflicts](../basics/route_conflicts.md). -| `::single-static-path-router` | Fastest possible router: valid only if there is one static route. -| `:prefix-tree-router` | TODO: https://github.com/julienschmidt/httprouter#how-does-it-work +| `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Works with any kind of routes. Slow, but works with all route trees. +| `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters and there are no [Route conflicts](../basics/route_conflicts.md). +| `:mixed-router` | Creates internally a `:prefix-tree-router` and a `:lookup-router` and used them to effectively get best-of-both-worlds. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). +| `::single-static-path-router` | Super fast router: sting-matches the route. Valid only if there is one static route. +| `:prefix-tree-router` | Router that creates a [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) out of an route table. Much faster than `:linear-router`. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). -The router name can be asked from the router +The router name can be asked from the router: ```clj +(require '[reitit.core :as r]) + +(def router + (r/router + [["/ping" ::ping] + ["/api/:users" ::users]])) + (r/router-name router) ; :mixed-router ``` diff --git a/doc/advanced/route_validation.md b/doc/advanced/route_validation.md index 80ef1bcd..90a84222 100644 --- a/doc/advanced/route_validation.md +++ b/doc/advanced/route_validation.md @@ -5,7 +5,7 @@ Namespace `reitit.spec` contains [clojure.spec](https://clojure.org/about/spec) **NOTE:** Use of specs requires to use one of the following: * `[org.clojure/clojurescript "1.9.660"]` (or higher) -* `[org.clojure/clojure "1.9.0-beta2"]` (or higher) +* `[org.clojure/clojure "1.9.0-RC1"]` (or higher) * `[clojure-future-spec "1.9.0-alpha17"]` (if Clojure 1.8 is used) ## Example diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 7efe387f..afeae96a 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,6 +1,7 @@ (ns reitit.core (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] + [reitit.trie :as trie] [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) #?(:clj (:import (reitit.impl Route)))) @@ -216,6 +217,46 @@ (if-let [match (impl/fast-get lookup name)] (match params))))))) +(defn prefix-tree-router + "Creates a prefix-tree router from resolved routes and optional + expanded options. See [[router]] for available options" + ([routes] + (prefix-tree-router routes {})) + ([routes opts] + (let [compiled (compile-routes routes opts) + names (find-names routes opts) + [node lookup] (reduce + (fn [[node lookup] [p {:keys [name] :as meta} result]] + (let [{:keys [params] :as route} (impl/create [p meta result]) + f #(if-let [path (impl/path-for route %)] + (->Match p meta result % path) + (->PartialMatch p meta result % params))] + [(trie/insert node p (->Match p meta result nil nil)) + (if name (assoc lookup name f) lookup)])) + [nil {}] compiled) + lookup (impl/fast-map lookup)] + (reify + Router + (router-name [_] + :prefix-tree-router) + (routes [_] + compiled) + (options [_] + opts) + (route-names [_] + names) + (match-by-path [_ path] + (if-let [match (trie/lookup node path {})] + (-> (:data match) + (assoc :params (:params match)) + (assoc :path path)))) + (match-by-name [_ name] + (if-let [match (impl/fast-get lookup name)] + (match nil))) + (match-by-name [_ name params] + (if-let [match (impl/fast-get lookup name)] + (match params))))))) + (defn single-static-path-router "Creates a fast router of 1 static route(s) and optional expanded options. See [[router]] for available options" @@ -252,16 +293,16 @@ (defn mixed-router "Creates two routers: [[lookup-router]] or [[single-static-path-router]] for - static routes and [[linear-router]] for wildcard routes. All + static routes and [[prefix-tree-router]] for wildcard routes. All routes should be non-conflicting. Takes resolved routes and optional expanded options. See [[router]] for options." ([routes] (mixed-router routes {})) ([routes opts] - (let [{linear true, lookup false} (group-by impl/wild-route? routes) + (let [{wild true, lookup false} (group-by impl/wild-route? routes) compiled (compile-routes routes opts) ->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router) - wildcard-router (linear-router linear opts) + wildcard-router (prefix-tree-router wild opts) static-router (->static-router lookup opts) names (find-names routes opts)] (reify Router @@ -309,10 +350,10 @@ router (cond router router (and (= 1 (count routes)) (not wilds?)) single-static-path-router + conflicting linear-router (not wilds?) lookup-router - all-wilds? linear-router - (not conflicting) mixed-router - :else linear-router)] + all-wilds? prefix-tree-router + :else mixed-router)] (when-let [conflicts (:conflicts opts)] (when conflicting (conflicts conflicting))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 75da4dcf..5694336f 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -69,10 +69,20 @@ ;; (c) https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj ;; -(defn- wild? [s] +(defn wild? [s] (contains? #{\: \*} (first s))) -(defn- partition-wilds +(defn wild-param? + "Return true if a string segment starts with a wildcard string." + [segment] + (= \: (first segment))) + +(defn catch-all-param? + "Return true if a string segment starts with a catch-all string." + [segment] + (= \* (first segment))) + +(defn partition-wilds "Given a path-spec string, return a seq of strings with wildcards and catch-alls separated into their own strings. Eats the forward slash following a wildcard." @@ -161,7 +171,7 @@ :cljs [[a k v] (assoc a k v)])) (defn fast-map [m] - #?@(:clj [(java.util.HashMap. m)] + #?@(:clj [(java.util.HashMap. (or m {}))] :cljs [m])) (defn fast-get diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc new file mode 100644 index 00000000..a0a14776 --- /dev/null +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -0,0 +1,223 @@ +(ns reitit.trie + (:require [reitit.impl :as impl])) + +;; +;; original https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj +;; + +(declare insert) + +(defn- char-key [s i] + (if (< i (count s)) + (subs s i (inc i)))) + +(defn- maybe-wild-node [children] + (get children ":")) + +(defn- maybe-catch-all-node [children] + (get children "*")) + +(defprotocol Node + (lookup [this path params]) + (get-segment [this]) + (update-segment [this subs lcs]) + (get-data [this]) + (set-data [this data]) + (get-chidren [this]) + (add-child [this key child]) + (insert-child [this key path-spec data])) + +(extend-protocol Node + nil + (lookup [_ _ _]) + (get-segment [_])) + +(defrecord Match [data params]) + +(defn- wild-node [segment param children data] + (let [?wild (maybe-wild-node children) + ?catch (maybe-catch-all-node children) + children' (impl/fast-map children)] + ^{:type ::node} + (reify + Node + (lookup [_ path params] + (let [i (.indexOf ^String path "/")] + (if (pos? i) + (let [value (subs path 0 i)] + (let [child (impl/fast-get children' (char-key path (inc i))) + path' (subs path (inc i)) + params (assoc params param value)] + (or (lookup child path' params) + (lookup ?wild path' params) + (lookup ?catch path' params)))) + (->Match data (assoc params param path))))) + (get-segment [_] + segment) + (get-data [_] + data) + (set-data [_ data] + (wild-node segment param children data)) + (get-chidren [_] + children) + (add-child [_ key child] + (wild-node segment param (assoc children key child) data)) + (insert-child [_ key path-spec child-data] + (wild-node segment param (update children key insert path-spec child-data) data))))) + +(defn- catch-all-node [segment children param data] + ^{:type ::node} + (reify + Node + (lookup [_ path params] + (->Match data (assoc params param path))) + (get-segment [_] + segment) + (get-data [_] + data) + (get-chidren [_] + children))) + +(defn- static-node [^String segment children data] + (let [size (count segment) + ?wild (maybe-wild-node children) + ?catch (maybe-catch-all-node children) + children' (impl/fast-map children)] + ^{:type ::node} + (reify + Node + (lookup [_ path params] + (if (#?(:clj .equals, :cljs =) segment path) + (->Match data params) + (let [p (if (>= (count path) size) (subs path 0 size))] + (if (#?(:clj .equals, :cljs =) segment p) + (let [child (impl/fast-get children' (char-key path size)) + path (subs path size)] + (or (lookup child path params) + (lookup ?wild path params) + (lookup ?catch path params))))))) + (get-segment [_] + segment) + (update-segment [_ subs lcs] + (static-node (subs segment lcs) children data)) + (get-data [_] + data) + (set-data [_ data] + (static-node segment children data)) + (get-chidren [_] + children) + (add-child [_ key child] + (static-node segment (assoc children key child) data)) + (insert-child [_ key path-spec child-data] + (static-node segment (update children key insert path-spec child-data) data))))) + +(defn- make-node + "Given a path-spec segment string and a payload object, return a new + tree node." + [segment data] + (cond + (impl/wild-param? segment) + (wild-node segment (keyword (subs segment 1)) nil data) + + (impl/catch-all-param? segment) + (catch-all-node segment (keyword (subs segment 1)) nil data) + + :else + (static-node segment nil data))) + +(defn- new-node + "Given a path-spec and a payload object, return a new tree node. If + the path-spec contains wildcards or catch-alls, will return parent + node of a tree (linked list)." + [path-spec data] + (if (impl/contains-wilds? path-spec) + (let [parts (impl/partition-wilds path-spec)] + (reduce (fn [child segment] + (when (impl/catch-all-param? segment) + (throw (ex-info "catch-all may only appear at the end of a path spec" + {:patch-spec path-spec}))) + (-> (make-node segment nil) + (add-child (subs (get-segment child) 0 1) child))) + (let [segment (last parts)] + (make-node segment data)) + (reverse (butlast parts)))) + (make-node path-spec data))) + +(defn- calc-lcs + "Given two strings, return the end index of the longest common + prefix string." + [s1 s2] + (loop [i 1] + (cond (or (< (count s1) i) + (< (count s2) i)) + (dec i) + + (= (subs s1 0 i) + (subs s2 0 i)) + (recur (inc i)) + + :else (dec i)))) + +(defn- split + "Given a node, a path-spec, a payload object to insert into the tree + and the lcs, split the node and return a new parent node with the + old contents of node and the new item as children. + lcs is the index of the longest common string in path-spec and the + segment of node." + [node path-spec data lcs] + (let [segment (get-segment node) + common (subs path-spec 0 lcs) + parent (new-node common nil)] + (if (= common path-spec) + (-> (set-data parent data) + (add-child (char-key segment lcs) (update-segment node subs lcs))) + (-> parent + (add-child (char-key segment lcs) (update-segment node subs lcs)) + (insert-child (char-key path-spec lcs) (subs path-spec lcs) data))))) + +(defn insert + "Given a tree node, a path-spec and a payload object, return a new + tree with payload inserted." + [node path-spec data] + (let [segment (get-segment node)] + (cond (nil? node) + (new-node path-spec data) + + (= segment path-spec) + (set-data node data) + + ;; handle case where path-spec is a wildcard param + (impl/wild-param? path-spec) + (let [lcs (calc-lcs segment path-spec) + common (subs path-spec 0 lcs)] + (if (= common segment) + (let [path-spec (subs path-spec (inc lcs))] + (insert-child node (subs path-spec 0 1) path-spec data)) + (throw (ex-info "route conflict" + {:node node + :path-spec path-spec + :segment segment})))) + + ;; in the case where path-spec is a catch-all, node should always be nil. + ;; getting here means we have an invalid route specification + (impl/catch-all-param? path-spec) + (throw (ex-info "route conflict" + {:node node + :path-spec path-spec + :segment segment})) + + :else + (let [lcs (calc-lcs segment path-spec)] + (cond (= lcs (count segment)) + (insert-child node (char-key path-spec lcs) (subs path-spec lcs) data) + + :else + (split node path-spec data lcs)))))) + +(defn view + "Returns a view representation of a prefix-tree." + [x] + (vec (concat + [(get-segment x)] + (some->> (get-chidren x) vals seq (map view)) + (some->> (get-data x) vector)))) diff --git a/perf-test/clj/reitit/opensensors_routing_test.clj b/perf-test/clj/reitit/opensensors_routing_test.clj index a9f194fb..cd3588dc 100644 --- a/perf-test/clj/reitit/opensensors_routing_test.clj +++ b/perf-test/clj/reitit/opensensors_routing_test.clj @@ -5,13 +5,14 @@ [cheshire.core :as json] [clojure.string :as str] [reitit.core :as reitit] - [reitit.core :as ring] + [reitit.ring :as ring] [bidi.bidi :as bidi] [ataraxy.core :as ataraxy] [compojure.api.sweet :refer [api routes context ANY]] + [compojure.core :as compojure] [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] @@ -72,19 +73,11 @@ total 10000 dropped (int (* total 0.45))] (mapv - #(let [times (->> (range total) - (mapv - (fn [_] - (let [now (System/nanoTime) - result (f %) - total (- (System/nanoTime) now)] - (assert result) - total))) - (sort) - (drop dropped) - (drop-last dropped)) - avg (int (/ (reduce + times) (count times)))] - [% avg]) urls))) + (fn [path] + (let [time (int (* (first (:sample-mean (cc/quick-benchmark (dotimes [_ 1000] (f path)) {}))) 1e6))] + (println path "=>" time "ns") + [path time])) + urls))) (defn bench [routes no-paths?] (let [routes (mapv (fn [[path name]] @@ -278,6 +271,82 @@ ["topics/" topic] [:test/route47 topic] "topics" [:test/route50]}})) +(def opensensors-compojure-routes + (compojure/routes + (compojure/context "/v1" [] + (compojure/context "/public" [] + (compojure/ANY "/topics/:topic" [] {:name :test/route4} handler) + (compojure/ANY "/users/:user-id" [] {:name :test/route16} handler) + (compojure/ANY "/orgs/:org-id" [] {:name :test/route18} handler)) + (compojure/context "/users/:user-id" [] + (compojure/ANY "/orgs/:org-id" [] {:name :test/route5} handler) + (compojure/ANY "/invitations" [] {:name :test/route7} handler) + (compojure/ANY "/topics" [] {:name :test/route9} handler) + (compojure/ANY "/bookmarks/followers" [] {:name :test/route10} handler) + (compojure/context "/devices" [] + (compojure/ANY "/" [] {:name :test/route15} handler) + #_(compojure/ANY "/bulk" [] {:name :test/route21} handler) + (compojure/ANY "/:client-id" [] {:name :test/route35} handler) + (compojure/ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) + (compojure/ANY "/device-errors" [] {:name :test/route22} handler) + (compojure/ANY "/usage-stats" [] {:name :test/route24} handler) + (compojure/ANY "/claim-device/:client-id" [] {:name :test/route26} handler) + (compojure/ANY "/owned-orgs" [] {:name :test/route31} handler) + (compojure/ANY "/bookmark/:topic" [] {:name :test/route33} handler) + (compojure/ANY "/" [] {:name :test/route36} handler) + (compojure/ANY "/orgs" [] {:name :test/route52} handler) + (compojure/ANY "/api-key" [] {:name :test/route43} handler) + (compojure/ANY "/bookmarks" [] {:name :test/route56} handler)) + (compojure/ANY "/search/topics/:term" [] {:name :test/route6} handler) + (compojure/context "/orgs" [] + (compojure/ANY "/" [] {:name :test/route55} handler) + (compojure/context "/:org-id" [] + (compojure/context "/devices" [] + (compojure/ANY "/" [] {:name :test/route37} handler) + (compojure/ANY "/:device-id" [] {:name :test/route13} handler) + #_(compojure/ANY "/:batch/:type" [] {:name :test/route8} handler)) + (compojure/ANY "/usage-stats" [] {:name :test/route12} handler) + (compojure/ANY "/invitations" [] {:name :test/route19} handler) + (compojure/context "/members" [] + (compojure/ANY "/:user-id" [] {:name :test/route34} handler) + (compojure/ANY "/" [] {:name :test/route38} handler) + #_(compojure/ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) + (compojure/ANY "/errors" [] {:name :test/route17} handler) + (compojure/ANY "/" [] {:name :test/route42} handler) + (compojure/ANY "/confirm-membership/:token" [] {:name :test/route46} handler) + (compojure/ANY "/topics" [] {:name :test/route57} handler))) + (compojure/context "/messages" [] + (compojure/ANY "/user/:user-id" [] {:name :test/route14} handler) + (compojure/ANY "/device/:client-id" [] {:name :test/route30} handler) + (compojure/ANY "/topic/:topic" [] {:name :test/route48} handler)) + (compojure/context "/topics" [] + (compojure/ANY "/:topic" [] {:name :test/route32} handler) + (compojure/ANY "/" [] {:name :test/route54} handler)) + (compojure/ANY "/whoami" [] {:name :test/route41} handler) + (compojure/ANY "/login" [] {:name :test/route51} handler)) + (compojure/context "/v2" [] + (compojure/ANY "/whoami" [] {:name :test/route1} handler) + (compojure/context "/users/:user-id" [] + (compojure/ANY "/datasets" [] {:name :test/route2} handler) + (compojure/ANY "/devices" [] {:name :test/route25} handler) + (compojure/context "/topics" [] + (compojure/ANY "/bulk" [] {:name :test/route29} handler) + (compojure/ANY "/" [] {:name :test/route54} handler)) + (compojure/ANY "/" [] {:name :test/route45} handler)) + (compojure/context "/public" [] + (compojure/context "/projects/:project-id" [] + (compojure/ANY "/datasets" [] {:name :test/route3} handler) + (compojure/ANY "/" [] {:name :test/route27} handler)) + #_(compojure/ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) + (compojure/ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) + (compojure/ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) + (compojure/ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) + (compojure/ANY "/login" [] {:name :test/route23} handler) + (compojure/ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) + (compojure/ANY "/schemas" [] {:name :test/route44} handler) + (compojure/ANY "/topics/:topic" [] {:name :test/route47} handler) + (compojure/ANY "/topics" [] {:name :test/route50} handler)))) + (def opensensors-compojure-api-routes (routes (context "/v1" [] @@ -490,25 +559,33 @@ #(app {:uri % :request-method :get})) bidi-f #(bidi/match-route opensensors-bidi-routes %) ataraxy-f #(ataraxy/matches opensensors-ataraxy-routes {:uri %}) + compojure-f #(opensensors-compojure-routes {:uri % :request-method :get}) compojure-api-f #(opensensors-compojure-api-routes {:uri % :request-method :get}) pedestal-f #(pedestal/find-route opensensors-pedestal-routes {:path-info % :request-method :get})] - ;; 2538ns -> 2028ns + ;; 2538ns + ;; 2065ns + ;; 680ns (prefix-tree-router) (bench!! routes true "reitit" reitit-f) - ;; 2845ns -> 2299ns + ;; 2845ns + ;; 2316ns + ;; 947ns (prefix-tree-router) (bench!! routes true "reitit-ring" reitit-ring-f) - ;; 2737ns + ;; 2541ns (bench!! routes true "pedestal" pedestal-f) - ;; 9823ns + ;; 9462ns (bench!! routes true "compojure-api" compojure-api-f) - ;; 16716ns + ;; 11041ns + (bench!! routes true "compojure" compojure-f) + + ;; 16820ns (bench!! routes true "bidi" bidi-f) - ;; 24467ns + ;; 24134ns (bench!! routes true "ataraxy" ataraxy-f))) (comment diff --git a/perf-test/clj/reitit/perf_test.clj b/perf-test/clj/reitit/perf_test.clj index 61b6ae31..dfefd636 100644 --- a/perf-test/clj/reitit/perf_test.clj +++ b/perf-test/clj/reitit/perf_test.clj @@ -148,6 +148,7 @@ (call)))) ;; 710 µs (3-18x) + ;; 540 µs (4-23x) -23% prefix-tree-router (title "reitit") (let [call #(reitit/match-by-path reitit-routes "/workspace/1/1")] (assert (call)) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj new file mode 100644 index 00000000..c607d675 --- /dev/null +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -0,0 +1,105 @@ +(ns reitit.prefix-tree-perf-test + (:require [clojure.test :refer :all] + [io.pedestal.http.route.prefix-tree :as p] + [reitit.trie :as trie] + [criterium.core :as cc])) + +;; +;; testing +;; + +(def routes + [["/v2/whoami" {:name :test/route1}] + ["/v2/users/:user-id/datasets" {:name :test/route2}] + ["/v2/public/projects/:project-id/datasets" {:name :test/route3}] + ["/v1/public/topics/:topic" {:name :test/route4}] + ["/v1/users/:user-id/orgs/:org-id" {:name :test/route5}] + ["/v1/search/topics/:term" {:name :test/route6}] + ["/v1/users/:user-id/invitations" {:name :test/route7}] + ["/v1/users/:user-id/topics" {:name :test/route9}] + ["/v1/users/:user-id/bookmarks/followers" {:name :test/route10}] + ["/v2/datasets/:dataset-id" {:name :test/route11}] + ["/v1/orgs/:org-id/usage-stats" {:name :test/route12}] + ["/v1/orgs/:org-id/devices/:client-id" {:name :test/route13}] + ["/v1/messages/user/:user-id" {:name :test/route14}] + ["/v1/users/:user-id/devices" {:name :test/route15}] + ["/v1/public/users/:user-id" {:name :test/route16}] + ["/v1/orgs/:org-id/errors" {:name :test/route17}] + ["/v1/public/orgs/:org-id" {:name :test/route18}] + ["/v1/orgs/:org-id/invitations" {:name :test/route19}] + ["/v1/users/:user-id/device-errors" {:name :test/route22}] + ["/v2/login" {:name :test/route23}] + ["/v1/users/:user-id/usage-stats" {:name :test/route24}] + ["/v2/users/:user-id/devices" {:name :test/route25}] + ["/v1/users/:user-id/claim-device/:client-id" {:name :test/route26}] + ["/v2/public/projects/:project-id" {:name :test/route27}] + ["/v2/public/datasets/:dataset-id" {:name :test/route28}] + ["/v2/users/:user-id/topics/bulk" {:name :test/route29}] + ["/v1/messages/device/:client-id" {:name :test/route30}] + ["/v1/users/:user-id/owned-orgs" {:name :test/route31}] + ["/v1/topics/:topic" {:name :test/route32}] + ["/v1/users/:user-id/bookmark/:topic" {:name :test/route33}] + ["/v1/orgs/:org-id/members/:user-id" {:name :test/route34}] + ["/v1/users/:user-id/devices/:client-id" {:name :test/route35}] + ["/v1/users/:user-id" {:name :test/route36}] + ["/v1/orgs/:org-id/devices" {:name :test/route37}] + ["/v1/orgs/:org-id/members" {:name :test/route38}] + ["/v2/orgs/:org-id/topics" {:name :test/route40}] + ["/v1/whoami" {:name :test/route41}] + ["/v1/orgs/:org-id" {:name :test/route42}] + ["/v1/users/:user-id/api-key" {:name :test/route43}] + ["/v2/schemas" {:name :test/route44}] + ["/v2/users/:user-id/topics" {:name :test/route45}] + ["/v1/orgs/:org-id/confirm-membership/:token" {:name :test/route46}] + ["/v2/topics/:topic" {:name :test/route47}] + ["/v1/messages/topic/:topic" {:name :test/route48}] + ["/v1/users/:user-id/devices/:client-id/reset-password" {:name :test/route49}] + ["/v2/topics" {:name :test/route50}] + ["/v1/login" {:name :test/route51}] + ["/v1/users/:user-id/orgs" {:name :test/route52}] + ["/v2/public/messages/dataset/:dataset-id" {:name :test/route53}] + ["/v1/topics" {:name :test/route54}] + ["/v1/orgs" {:name :test/route55}] + ["/v1/users/:user-id/bookmarks" {:name :test/route56}] + ["/v1/orgs/:org-id/topics" {:name :test/route57}]]) + +(def pedestal-tree + (reduce + (fn [acc [p d]] + (p/insert acc p d)) + nil routes)) + +(def reitit-tree + (reduce + (fn [acc [p d]] + (trie/insert acc p d)) + nil routes)) + +(defn bench! [] + + ;; 2.3ms + (cc/quick-bench + (dotimes [_ 1000] + (p/lookup pedestal-tree "/v1/orgs/1/topics"))) + + ;; 3.1ms + ;; 2.5ms (string equals) + ;; 2.5ms (protocol) + ;; 2.3ms (nil childs) + ;; 2.0ms (rando impros) + ;; 1.9ms (wild & catch shortcuts) + ;; 1.5ms (inline child fetching) + ;; 1.5ms (WildNode also backtracks) + ;; 1.4ms (precalculate segment-size) + ;; 1.3ms (fast-map) + ;; 1.3ms (dissoc wild & catch-all from children) + ;; 1.3ms (reified protocols) + ;; 0.8ms (flattened matching) + ;; 0.8ms (return route-data) + ;; 0.8ms (fix payloads) + (cc/quick-bench + (dotimes [_ 1000] + (trie/lookup reitit-tree "/v1/orgs/1/topics" {})))) + +(comment + (bench!)) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 669b988c..5e770e1f 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -7,106 +7,80 @@ (deftest reitit-test - (testing "linear-router" - (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]])] - (is (= :linear-router (r/router-name router))) - (is (= [["/api/ipa/:size" {:name ::beer} nil]] - (r/routes router))) - (is (= true (map? (r/options router)))) - (is (= (r/map->Match - {:template "/api/ipa/:size" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-path router "/api/ipa/large"))) - (is (= (r/map->Match - {:template "/api/ipa/:size" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-name router ::beer {:size "large"}))) - (is (= nil (r/match-by-name router "ILLEGAL"))) - (is (= [::beer] (r/route-names router))) - - (testing "name-based routing with missing parameters" - (is (= (r/map->PartialMatch + (testing "routers handling wildcard paths" + (are [r name] + (let [router (r/router ["/api" ["/ipa" ["/:size" ::beer]]] {:router r})] + (is (= name (r/router-name router))) + (is (= [["/api/ipa/:size" {:name ::beer} nil]] + (r/routes router))) + (is (= true (map? (r/options router)))) + (is (= (r/map->Match {:template "/api/ipa/:size" :meta {:name ::beer} - :required #{:size} - :params nil}) - (r/match-by-name router ::beer))) - (is (= true (r/partial-match? (r/match-by-name router ::beer)))) - (is (thrown-with-msg? - ExceptionInfo - #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" - (r/match-by-name! router ::beer)))))) + :path "/api/ipa/large" + :params {:size "large"}}) + (r/match-by-path router "/api/ipa/large"))) + (is (= (r/map->Match + {:template "/api/ipa/:size" + :meta {:name ::beer} + :path "/api/ipa/large" + :params {:size "large"}}) + (r/match-by-name router ::beer {:size "large"}))) + (is (= nil (r/match-by-name router "ILLEGAL"))) + (is (= [::beer] (r/route-names router))) - (testing "lookup-router" - (let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]] {:router r/lookup-router})] - (is (= :lookup-router (r/router-name router))) - (is (= [["/api/ipa/large" {:name ::beer} nil]] - (r/routes router))) - (is (= true (map? (r/options router)))) - (is (= (r/map->Match - {:template "/api/ipa/large" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {}}) - (r/match-by-path router "/api/ipa/large"))) - (is (= (r/map->Match - {:template "/api/ipa/large" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-name router ::beer {:size "large"}))) - (is (= nil (r/match-by-name router "ILLEGAL"))) - (is (= [::beer] (r/route-names router))) + (testing "name-based routing with missing parameters" + (is (= (r/map->PartialMatch + {:template "/api/ipa/:size" + :meta {:name ::beer} + :required #{:size} + :params nil}) + (r/match-by-name router ::beer))) + (is (= true (r/partial-match? (r/match-by-name router ::beer)))) + (is (thrown-with-msg? + ExceptionInfo + #"^missing path-params for route /api/ipa/:size -> \#\{:size\}$" + (r/match-by-name! router ::beer))))) - (testing "can't be created with wildcard routes" - (is (thrown-with-msg? - ExceptionInfo - #"can't create :lookup-router with wildcard routes" - (r/lookup-router - (r/resolve-routes - ["/api/:version/ping"] {}))))))) + r/linear-router :linear-router + r/prefix-tree-router :prefix-tree-router + r/mixed-router :mixed-router)) - (testing "single-static-path-router" - (let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]])] - (is (= :single-static-path-router (r/router-name router))) - (is (= [["/api/ipa/large" {:name ::beer} nil]] - (r/routes router))) - (is (= true (map? (r/options router)))) - (is (= (r/map->Match - {:template "/api/ipa/large" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {}}) - (r/match-by-path router "/api/ipa/large"))) - (is (= (r/map->Match - {:template "/api/ipa/large" - :meta {:name ::beer} - :path "/api/ipa/large" - :params {:size "large"}}) - (r/match-by-name router ::beer {:size "large"}))) - (is (= nil (r/match-by-name router "ILLEGAL"))) - (is (= [::beer] (r/route-names router))) + (testing "routers handling static paths" + (are [r name] + (let [router (r/router ["/api" ["/ipa" ["/large" ::beer]]] {:router r})] + (is (= name (r/router-name router))) + (is (= [["/api/ipa/large" {:name ::beer} nil]] + (r/routes router))) + (is (= true (map? (r/options router)))) + (is (= (r/map->Match + {:template "/api/ipa/large" + :meta {:name ::beer} + :path "/api/ipa/large" + :params {}}) + (r/match-by-path router "/api/ipa/large"))) + (is (= (r/map->Match + {:template "/api/ipa/large" + :meta {:name ::beer} + :path "/api/ipa/large" + :params {:size "large"}}) + (r/match-by-name router ::beer {:size "large"}))) + (is (= nil (r/match-by-name router "ILLEGAL"))) + (is (= [::beer] (r/route-names router))) - (testing "can't be created with wildcard routes" - (is (thrown-with-msg? - ExceptionInfo - #":single-static-path-router requires exactly 1 static route" - (r/single-static-path-router - (r/resolve-routes - ["/api/:version/ping"] {}))))) + (testing "can't be created with wildcard routes" + (is (thrown-with-msg? + ExceptionInfo + #"can't create :lookup-router with wildcard routes" + (r/lookup-router + (r/resolve-routes + ["/api/:version/ping"] {})))))) - (testing "can't be created with multiple routes" - (is (thrown-with-msg? - ExceptionInfo - #":single-static-path-router requires exactly 1 static route" - (r/single-static-path-router - (r/resolve-routes - [["/ping"] - ["/pong"]] {}))))))) + r/lookup-router :lookup-router + r/single-static-path-router :single-static-path-router + r/linear-router :linear-router + r/prefix-tree-router :prefix-tree-router + r/mixed-router :mixed-router)) (testing "route coercion & compilation"