diff --git a/doc/images/opensensors.png b/doc/images/opensensors.png index 9f65dff4..fd922808 100644 Binary files a/doc/images/opensensors.png and b/doc/images/opensensors.png differ diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 515cf039..7760a113 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -40,9 +40,14 @@ public class Trie { } public static class Match { - public IPersistentMap params = PersistentArrayMap.EMPTY; + public IPersistentMap params; public Object data; + public Match(IPersistentMap params, Object data) { + this.params = params; + this.data = data; + } + @Override public String toString() { Map m = new HashMap<>(); @@ -53,7 +58,7 @@ public class Trie { } public interface Matcher { - Match match(int i, int max, char[] path, Match match); + Match match(int i, int max, char[] path); int depth(); @@ -76,7 +81,7 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (max < i + size) { return null; } @@ -85,7 +90,7 @@ public class Trie { return null; } } - return child.match(i + size, max, path, match); + return child.match(i + size, max, path); } @Override @@ -104,22 +109,23 @@ public class Trie { } } - public static DataMatcher dataMatcher(Object data) { - return new DataMatcher(data); + public static DataMatcher dataMatcher(IPersistentMap params, Object data) { + return new DataMatcher(params, data); } static final class DataMatcher implements Matcher { + private final IPersistentMap params; private final Object data; - DataMatcher(Object data) { + DataMatcher(IPersistentMap params, Object data) { + this.params = params; this.data = data; } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i == max) { - match.data = data; - return match; + return new Match(params, data); } return null; } @@ -156,7 +162,7 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i < max && path[i] != end) { int stop = max; for (int j = i; j < max; j++) { @@ -166,7 +172,7 @@ public class Trie { break; } } - final Match m = child.match(stop, max, path, match); + final Match m = child.match(stop, max, path); if (m != null) { m.params = m.params.assoc(key, decode(path, i, stop)); } @@ -191,25 +197,25 @@ public class Trie { } } - public static CatchAllMatcher catchAllMatcher(Keyword parameter, Object data) { - return new CatchAllMatcher(parameter, data); + public static CatchAllMatcher catchAllMatcher(Keyword parameter, IPersistentMap params, Object data) { + return new CatchAllMatcher(parameter, params, data); } static final class CatchAllMatcher implements Matcher { private final Keyword parameter; + private final IPersistentMap params; private final Object data; - CatchAllMatcher(Keyword parameter, Object data) { + CatchAllMatcher(Keyword parameter, IPersistentMap params, Object data) { this.parameter = parameter; + this.params = params; this.data = data; } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i < max) { - match.params = match.params.assoc(parameter, decode(path, i, max)); - match.data = data; - return match; + return new Match(params.assoc(parameter, decode(path, i, max)), data); } return null; } @@ -226,7 +232,7 @@ public class Trie { @Override public String toString() { - return "[" + parameter + " " + new DataMatcher(data) + "]"; + return "[" + parameter + " " + new DataMatcher(null, data) + "]"; } } @@ -246,9 +252,9 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { for (int j = 0; j < size; j++) { - final Match m = childs[j].match(i, max, path, match); + final Match m = childs[j].match(i, max, path); if (m != null) { return m; } @@ -273,7 +279,7 @@ public class Trie { } public static Object lookup(Matcher matcher, String path) { - return matcher.match(0, path.length(), path.toCharArray(), new Match()); + return matcher.match(0, path.length(), path.toCharArray()); } public static void main(String[] args) { @@ -283,8 +289,8 @@ public class Trie { staticMatcher("/auth/", linearMatcher( Arrays.asList( - staticMatcher("login", dataMatcher(1)), - staticMatcher("recovery", dataMatcher(2))))))); + staticMatcher("login", dataMatcher(null, 1)), + staticMatcher("recovery", dataMatcher(null, 2))))))); System.err.println(matcher); System.out.println(lookup(matcher, "/auth/login")); System.out.println(lookup(matcher, "/auth/recovery")); diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 9c05ba5f..fbab995f 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -131,7 +131,7 @@ (match-by-path [_ path] (if-let [match (trie/lookup matcher path)] (-> (:data match) - (assoc :path-params (:path-params match)) + (assoc :path-params (:params match)) (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] @@ -220,7 +220,7 @@ (match-by-path [_ path] (if-let [match (trie/lookup pl path)] (-> (:data match) - (assoc :path-params (:path-params match)) + (assoc :path-params (:params match)) (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 23228689..cd04da2e 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -7,8 +7,8 @@ (defrecord Wild [value]) (defrecord CatchAll [value]) -(defrecord Match [data path-params]) -(defrecord Node [children wilds catch-all data]) +(defrecord Match [params data]) +(defrecord Node [children wilds catch-all params data]) (defn wild? [x] (instance? Wild x)) (defn catch-all? [x] (instance? CatchAll x)) @@ -19,9 +19,9 @@ (depth [this]) (length [this])) -(defn assoc-path-param [match k v] - (let [params (:path-params match)] - (assoc match :path-params (assoc params k v)))) +(defn assoc-param [match k v] + (let [params (:params match)] + (assoc match :params (assoc params k v)))) ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix (defn common-prefix [s1 s2] @@ -121,25 +121,25 @@ ;; (defn- -node [m] - (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) + (map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m))) -(defn- -insert [node [path & ps] data] +(defn- -insert [node [path & ps] params data] (let [node' (cond (nil? path) - (assoc node :data data) + (assoc node :data data :params params) (instance? Wild path) (let [next (first ps)] (if (or (instance? Wild next) (instance? CatchAll next)) (ex/fail! (str "Two following wilds: " path ", " next)) - (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))))) + (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data))))) (instance? CatchAll path) - (assoc-in node [:catch-all path] (-node {:data data})) + (assoc-in node [:catch-all path] (-node {:params params, :data data})) (str/blank? path) - (-insert node ps data) + (-insert node ps params data) :else (or @@ -148,20 +148,20 @@ (if-let [cp (common-prefix p path)] (if (= cp p) ;; insert into child node - (let [n' (-insert n (conj ps (subs path (count p))) data)] + (let [n' (-insert n (conj ps (subs path (count p))) params data)] (reduced (assoc-in node [:children p] n'))) ;; split child node (let [rp (subs p (count cp)) rp' (subs path (count cp)) - n' (-insert (-node {}) ps data) - n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil)] + n' (-insert (-node {}) ps params data) + n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)] (reduced (update node :children (fn [children] (-> children (dissoc p) (assoc cp n''))))))))) nil (:children node)) ;; new child node - (assoc-in node [:children path] (-insert (-node {}) ps data))))] + (assoc-in node [:children path] (-insert (-node {}) ps params data))))] (if-let [child (get-in node' [:children ""])] ;; optimize by removing empty paths (-> (merge-with merge (dissoc node' :data) child) @@ -173,9 +173,9 @@ (let [param (subs path start end)] (if percent? (js/decodeURIComponent param) param)))) -(defn data-matcher [data] - #?(:clj (Trie/dataMatcher data) - :cljs (let [match (->Match data {})] +(defn data-matcher [params data] + #?(:clj (Trie/dataMatcher params data) + :cljs (let [match (->Match params data)] (reify Matcher (match [_ i max _] (if (= i max) @@ -207,23 +207,23 @@ (loop [percent? false, j i] (if (= max j) (if-let [match (match matcher max max path)] - (assoc-path-param match key (decode path i max percent?))) + (assoc-param match key (decode path i max percent?))) (let [c ^char (get path j)] (condp = c end (if-let [match (match matcher j max path)] - (assoc-path-param match key (decode path i j percent?))) + (assoc-param match key (decode path i j percent?))) \% (recur true (inc j)) (recur percent? (inc j)))))))) (view [_] [key (view matcher)]) (depth [_] (inc (depth matcher))) (length [_])))) -(defn catch-all-matcher [key data] - #?(:clj (Trie/catchAllMatcher key data) - :cljs (let [match (->Match data nil)] +(defn catch-all-matcher [key params data] + #?(:clj (Trie/catchAllMatcher key params data) + :cljs (let [match (->Match params data)] (reify Matcher (match [_ i max path] - (if (< i max) (assoc-path-param match key (decode path i max true)))) + (if (< i max) (assoc-param match key (decode path i max true)))) (view [_] [key [data]]) (depth [_] 1) (length [_]))))) @@ -255,12 +255,14 @@ (insert acc p d)) node routes)) ([node path data] - (-insert (or node (-node {})) (split-path path) data))) + (let [parts (split-path path) + params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] + (-insert (or node (-node {})) (split-path path) params data)))) -(defn compile [{:keys [data children wilds catch-all]}] +(defn compile [{:keys [data params children wilds catch-all] :or {params {}}}] (let [ends (fn [{:keys [children]}] (or (keys children) ["/"])) matchers (-> [] - (cond-> data (conj (data-matcher data))) + (cond-> data (conj (data-matcher params data))) (into (for [[p c] children] (static-matcher p (compile c)))) (into (for [[p c] wilds] @@ -269,11 +271,11 @@ (if (next ends) (ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends)) (wild-matcher p (ffirst ends) (compile c)))))) - (into (for [[p c] catch-all] (catch-all-matcher (:value p) (:data c)))))] + (into (for [[p c] catch-all] (catch-all-matcher (:value p) params (:data c)))))] (cond (> (count matchers) 1) (linear-matcher matchers) (= (count matchers) 1) (first matchers) - :else (data-matcher nil)))) + :else (data-matcher {} nil)))) (defn pretty [matcher] #?(:clj (-> matcher str read-string eval) @@ -281,9 +283,9 @@ (defn lookup [matcher path] #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] - (->Match (.data match) (.params match))) + (->Match (.params match) (.data match))) :cljs (if-let [match (match matcher 0 (count path) path)] - (->Match (:data match) (:path-params match))))) + (->Match (:params match) (:data match))))) ;; ;; spike diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 5f49558b..59838449 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -333,6 +333,7 @@ ;; 281ns (trie-router, no injects, optimized) ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 273ns (trie-router, no injects, direct-data) + ;; 256ns (trie-router, pre-defined parameters) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -347,8 +348,9 @@ ;; 66µs (trie-router, no injects) ;; 64µs (trie-router, no injects, optimized) - 124µs (clojure) ;; 63µs (trie-router, no injects, switch-case) - 124µs (clojure) - ;; 63ns (trie-router, no injects, direct-data) - ;; 54ns (trie-router, non-transient params) + ;; 63µs (trie-router, no injects, direct-data) + ;; 54µs (trie-router, non-transient params) + ;; 49µs (trie-router, pre-defined parameters) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index d74a9764..c00a89a6 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -573,6 +573,7 @@ ;; 194ns (trie) ;; 160ns (trie, prioritized) ;; 130ns (trie, non-transient, direct-data) + ;; 121ns (trie, pre-defined parameters) (b! "reitit" reitit-f) ;; 2845ns @@ -587,12 +588,14 @@ ;; 323ns (trie, prioritized) ;; 289ns (trie, prioritized, zero-copy) ;; 266ns (trie, non-transient, direct-data) + ;; 251ns (trie, pre-defined parameters) (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) ;; 240ns (trie, prioritized) ;; 214ns (trie, non-transient, direct-data) + ;; 187ns (trie, pre-defined parameters) (b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) @@ -631,6 +634,7 @@ ;; 409ns (transient) ;; 409ns (staticMultiMatcher) ;; 305ns (non-persistent-params) + ;; 293ns (pre-defined parameters) (let [app (ring/ring-handler (ring/router opensensors-routes) {:inject-match? false, :inject-router? false}) request {:uri "/v1/users/1/devices/1", :request-method :get}] (doseq [[p r] (-> app (ring/get-router) (r/routes))] @@ -643,6 +647,7 @@ ; "Elapsed time: 9183.657012 msecs" ; "Elapsed time: 8674.70132 msecs" ; "Elapsed time: 6714.434915 msecs" + ; "Elapsed time: 6325.310043 msecs" (time (dotimes [_ 20000000] (app request))) diff --git a/perf-test/clj/reitit/perf_utils.clj b/perf-test/clj/reitit/perf_utils.clj index d2427903..49d0c485 100644 --- a/perf-test/clj/reitit/perf_utils.clj +++ b/perf-test/clj/reitit/perf_utils.clj @@ -62,7 +62,7 @@ (println) (let [times (for [[path [mean lower]] (bench-routes routes req f)] (do - (when verbose? (println (format "%7s %7s" mean lower) "\t" path)) + (when verbose? (println (format "%7s\t%7s" mean lower) "\t" path)) [mean lower]))] (title (str "average, lower/mean: " (int (/ (reduce + (map second times)) (count times))) "/" diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index b56981dd..291937c5 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -14,24 +14,24 @@ "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) (deftest tests - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/compile) (trie/lookup "/foo")))) - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) (trie/lookup "/foo")))) - (is (= (trie/->Match {:b 1} {:bar "bar"}) + (is (= (trie/->Match {:bar "bar"} {:b 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) (trie/lookup "/foo/bar")))) - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "" {:a 1}) (trie/compile) (trie/lookup "")))))