diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 88186d2b..47bf2e91 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -2,10 +2,10 @@ package reitit; import clojure.lang.Keyword; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.util.*; -import static java.util.Arrays.asList; - public class Trie { public static ArrayList split(final String path) { @@ -23,8 +23,22 @@ public class Trie { return segments; } + static String encode(String s) { + try { + if (s.contains("%")) { + String _s = s; + if (s.contains("+")) { + _s = s.replace("+", "%2B"); + } + return URLEncoder.encode(_s, "UTF-8"); + } + } catch (UnsupportedEncodingException ignored) { + } + return s; + } + public static class Match { - public Map params = new HashMap<>(); + public final Map params = new HashMap<>(); public Object data; @Override @@ -41,59 +55,6 @@ public class Trie { private Map catchAll = new HashMap<>(); private Object data; - @Override - public String toString() { - Map m = new HashMap<>(); - m.put(Keyword.intern("childs"), childs); - m.put(Keyword.intern("wilds"), wilds); - m.put(Keyword.intern("catchAll"), catchAll); - m.put(Keyword.intern("data"), data); - return m.toString(); - } - - public static Match lookup(Trie root, String path) { - return lookup(root, new Match(), split(path)); - } - - private static Match lookup(Trie root, Match match, List parts) { - Trie childTrie = null; - if (parts.isEmpty()) { - return match; - } else { - Trie trie = root; - int i = 0; - for (final String part : parts) { - i++; - childTrie = trie.childs.get(part); - if (childTrie != null) { - trie = childTrie; - } else { - for (final Map.Entry e : trie.wilds.entrySet()) { - childTrie = e.getValue(); - match.data = childTrie.data; - Match m = lookup(childTrie, match, parts.subList(i, parts.size())); - if (m != null) { - match.params.put(e.getKey(), part); - return m; - } - } - for (Map.Entry e : trie.catchAll.entrySet()) { - childTrie = e.getValue(); - match.params.put(e.getKey(), String.join("/", parts.subList(i - 1, parts.size()))); - match.data = childTrie.data; - return match; - } - break; - } - } - } - if (childTrie != null) { - match.data = childTrie.data; - return match; - } - return null; - } - public Trie add(String path, Object data) { List paths = split(path); Trie pointer = this; @@ -127,33 +88,247 @@ public class Trie { return this; } + private Matcher staticMatcher() { + if (childs.size() == 1) { + return new StaticMatcher(childs.keySet().iterator().next(), childs.values().iterator().next().matcher()); + } else { + Map m = new HashMap<>(); + for (Map.Entry e : childs.entrySet()) { + m.put(e.getKey(), e.getValue().matcher()); + } + return new StaticMapMatcher(m); + } + } + + public Matcher matcher() { + Matcher m; + if (!catchAll.isEmpty()) { + m = new CatchAllMatcher(catchAll.keySet().iterator().next(), catchAll.values().iterator().next().data); + } else if (!wilds.isEmpty()) { + List matchers = new ArrayList<>(); + if (data != null) { + matchers.add(new DataMatcher(data)); + } + if (!childs.isEmpty()) { + matchers.add(staticMatcher()); + } + for (Map.Entry e : wilds.entrySet()) { + matchers.add(new WildMatcher(e.getKey(), e.getValue().matcher())); + } + m = new LinearMatcher(matchers); + } else if (!childs.isEmpty()) { + m = staticMatcher(); + } else { + return new DataMatcher(data); + } + if (data != null) { + m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m)); + } + return m; + } + + public interface Matcher { + Match match(int i, List segments, Match match); + } + + public static final class StaticMatcher implements Matcher { + private final String segment; + private final Matcher child; + + StaticMatcher(String segment, Matcher child) { + this.segment = segment; + this.child = child; + } + + @Override + public Match match(int i, List segments, Match match) { + if (i < segments.size() && segment.equals(segments.get(i))) { + return child.match(i + 1, segments, match); + } + return null; + } + + @Override + public String toString() { + return "[\"" + segment + "\" " + child + "]"; + } + } + + public static final class WildMatcher implements Matcher { + private final Keyword parameter; + private final Matcher child; + + WildMatcher(Keyword parameter, Matcher child) { + this.parameter = parameter; + this.child = child; + } + + @Override + public Match match(int i, List segments, Match match) { + final Match m = child.match(i + 1, segments, match); + if (m != null) { + m.params.put(parameter, encode(segments.get(i))); + return m; + } + return null; + } + + @Override + public String toString() { + return "[" + parameter + " " + child + "]"; + } + } + + public static final class CatchAllMatcher implements Matcher { + private final Keyword parameter; + private final Object data; + + CatchAllMatcher(Keyword parameter, Object data) { + this.parameter = parameter; + this.data = data; + } + + @Override + public Match match(int i, List segments, Match match) { + match.params.put(parameter, encode(String.join("/", segments.subList(i, segments.size())))); + match.data = data; + return match; + } + + @Override + public String toString() { + return "[" + parameter + " " + new DataMatcher(data) + "]"; + } + } + + public static final class StaticMapMatcher implements Matcher { + private final Map map; + + StaticMapMatcher(Map map) { + this.map = map; + } + + @Override + public Match match(int i, List segments, Match match) { + final Matcher child = map.get(segments.get(i)); + if (child != null) { + return child.match(i + 1, segments, match); + } + return null; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("{"); + List keys = new ArrayList<>(map.keySet()); + for (int i = 0; i < keys.size(); i++) { + String path = keys.get(i); + Matcher value = map.get(path); + b.append("\"").append(path).append("\" ").append(value); + if (i < keys.size() - 1) { + b.append(", "); + } + } + b.append("}"); + return b.toString(); + } + } + + public static final class LinearMatcher implements Matcher { + + private final List childs; + + LinearMatcher(List childs) { + this.childs = childs; + } + + @Override + public Match match(int i, List segments, Match match) { + for (Matcher child : childs) { + final Match m = child.match(i, segments, match); + if (m != null) { + return m; + } + } + return null; + } + + @Override + public String toString() { + return childs.toString(); + } + } + + public static final class DataMatcher implements Matcher { + private final Object data; + + DataMatcher(Object data) { + this.data = data; + } + + @Override + public Match match(int i, List segments, Match match) { + if (i == segments.size()) { + match.data = data; + return match; + } + return null; + } + + @Override + public String toString() { + return (data != null ? data.toString() : "null"); + } + } + + public static Match lookup(Matcher matcher, String path) { + return matcher.match(0, split(path), new Match()); + } + + public static Matcher sample() { + Map m1 = new HashMap<>(); + m1.put("profile", new WildMatcher(Keyword.intern("type"), new DataMatcher(1))); + m1.put("permissions", new DataMatcher(2)); + + Map m2 = new HashMap<>(); + m2.put("user", new WildMatcher(Keyword.intern("id"), new StaticMapMatcher(m1))); + m2.put("company", new WildMatcher(Keyword.intern("cid"), new StaticMatcher("dept", new WildMatcher(Keyword.intern("did"), new DataMatcher(3))))); + m2.put("public", new CatchAllMatcher(Keyword.intern("*"), 4)); + m2.put("kikka", new LinearMatcher(Arrays.asList(new StaticMatcher("ping", new DataMatcher(5)), new WildMatcher(Keyword.intern("id"), new StaticMatcher("ping", new DataMatcher(6)))))); + return new StaticMapMatcher(m2); } public static void main(String[] args) { - Trie trie = - new Trie() - .add("/kikka", 1) - .add("/kakka", 2) - .add("/api/ping", 3) - .add("/api/pong", 4) - .add("/api/ipa/ping", 5) - .add("/api/ipa/pong", 6) - .add("/users/:user-id", 7) - .add("/users/:user-id/orders", 8) - .add("/users/:user-id/price", 9) - .add("/orders/:id/price", 10) - .add("/orders/:super", 11) - .add("/orders/:super/hyper/:giga", 12); - //System.out.println(lookup(trie, split("/kikka"))); - System.out.println(lookup(trie, "/orders/mies/hyper/peikko")); + Trie trie = new Trie(); + //trie.add("/kikka/:id/permissions", 1); + trie.add("/kikka/:id", 2); + trie.add("/kakka/ping", 3); + Matcher m = trie.matcher(); + System.err.println(m); + System.out.println(lookup(m, "/kikka/1/permissions")); + System.out.println(lookup(m, "/kikka/1")); - System.out.println(lookup( - new Trie().add("/user/:id/profile/:type/", 1), - "/user/1/profile/compat")); + /* + Trie trie = new Trie(); + trie.add("/user/:id/profile/:type", 1); + trie.add("/user/:id/permissions", 2); + trie.add("/company/:cid/dept/:did", 3); + trie.add("/this/is/a/static/route", 4); + Matcher m = trie.matcher(); + System.out.println(m); - System.out.println(lookup( - new Trie().add("/user/*path", 1), - "/user/1/profile/compat")); + System.err.println(lookup(m, "/this/is/a/static/route")); + System.err.println(lookup(m, "/user/1234/profile/compact")); + System.err.println(lookup(m, "/company/1234/dept/5678")); + System.err.println(); + */ + /* + System.err.println(lookup(sample(), "/user/1234/profile/compact")); + System.err.println(lookup(sample(), "/public/images/logo.jpg")); + System.err.println(lookup(sample(), "/kikka/ping")); + System.err.println(lookup(sample(), "/kikka/kukka/ping")); + */ } } diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index 82934543..fe83c7d5 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -8,4 +8,5 @@ :plugins [[lein-parent "0.3.2"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} + :java-source-paths ["java-src"] :dependencies [[meta-merge]]) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 834103bb..899410fc 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -271,6 +271,7 @@ (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) + pl (segment/compile pl) lookup (impl/fast-map nl) routes (uncompile-routes compiled-routes)] ^{:type ::router} @@ -288,9 +289,7 @@ names) (match-by-path [_ path] (if-let [match (segment/lookup pl path)] - (-> (:data match) - (assoc :path-params (impl/url-decode-coll (:path-params match))) - (assoc :path path)))) + (assoc (:data match) :path path))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] (match nil))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 3ba337ab..a4317999 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -5,7 +5,8 @@ #?(:clj (:import (java.util.regex Pattern) (java.util HashMap Map) - (java.net URLEncoder URLDecoder)))) + (java.net URLEncoder URLDecoder) + (reitit Trie)))) (defn maybe-map-values "Applies a function to every value of a map, updates the value if not nil. @@ -19,6 +20,10 @@ coll coll)) +(defn segments [path] + #?(:clj (Trie/split ^String path) + :cljs (.split path #"/" 666))) + ;; ;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj ;; @@ -42,10 +47,6 @@ (defn wild-or-catch-all-param? [x] (boolean (or (wild-param x) (catch-all-param x)))) -(defn segments [path] - #?(:clj (.split ^String path "/" 666) - :cljs (.split path #"/" 666))) - (defn contains-wilds? [path] (boolean (some wild-or-catch-all-param? (segments path)))) diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index 98173c16..18a5638d 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -1,5 +1,5 @@ (ns reitit.segment - (:refer-clojure :exclude [-lookup]) + (:refer-clojure :exclude [-lookup compile]) (:require [reitit.impl :as impl] [clojure.string :as str]) #?(:clj (:import (reitit Trie Trie$Match)))) @@ -44,18 +44,17 @@ (if (and wilds? (not (str/blank? p))) (some #(-lookup (impl/fast-get children' %) ps (assoc path-params % p)) wilds)) (if catch-all (-catch-all children' catch-all path-params p ps))))))))) +;; +;; public api +;; + (defn insert [root path data] #?(:cljs (-insert (or root (segment)) (impl/segments path) (map->Match {:data data})) - :clj (.add (or ^Trie root ^Trie (Trie.)) ^String path data))) + :clj (.add (or ^Trie root ^Trie (Trie.)) ^String path data))) -(defn create [paths] - (reduce - (fn [segment [p data]] - #?(:cljs (insert segment p data) - :clj (.add ^Trie segment ^String p data))) - #?(:cljs nil - :clj (Trie.)) - paths)) +(defn compile [segment] + #?(:cljs segment + :clj (.matcher ^Trie segment))) (defn lookup [segment path] #?(:cljs (-lookup segment (impl/segments path) {}) diff --git a/perf-test/clj/reitit/bide_perf_test.clj b/perf-test/clj/reitit/bide_perf_test.clj index be26dcde..b96d971d 100644 --- a/perf-test/clj/reitit/bide_perf_test.clj +++ b/perf-test/clj/reitit/bide_perf_test.clj @@ -89,10 +89,11 @@ ;; 1600 µs (title "bidi") - (assert (bidi/match-route bidi-routes "/auth/login")) - (cc/quick-bench - (dotimes [_ 1000] - (bidi/match-route bidi-routes "/auth/login"))) + (let [request "/auth/login"] + (assert (bidi/match-route bidi-routes request)) + (cc/quick-bench + (dotimes [_ 1000] + (bidi/match-route bidi-routes request)))) ;; 1400 µs (title "ataraxy") @@ -105,10 +106,10 @@ ;; 1000 µs (title "pedestal - map-tree => prefix-tree") (let [request {:path-info "/auth/login" :request-method :get}] - (assert (pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get})) + (assert (pedestal/find-route pedestal-router request)) (cc/quick-bench (dotimes [_ 1000] - (pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get})))) + (pedestal/find-route pedestal-router request)))) ;; 1400 µs (title "compojure") @@ -163,6 +164,7 @@ ;; 710 µs (3-18x) ;; 530 µs (4-24x) -25% prefix-tree-router ;; 710 µs (3-18x) segment-router + ;; 320 µs (6-40x) java-segment-router (title "reitit") (assert (reitit/match-by-path reitit-routes "/workspace/1/1")) (cc/quick-bench diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 88e546c3..1114f573 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -296,7 +296,9 @@ (def app (ring/ring-handler (ring/router - (reduce (partial add h) [] routes)))) + (reduce (partial add h) [] routes)) + (ring/create-default-handler) + {:inject-match? false, :inject-router? false})) (defrecord Req [uri request-method]) @@ -313,6 +315,8 @@ ;; 40ns (httprouter) ;; 140ns ;; 120ns (faster decode params) + ;; 140µs (java-segment-router) + ;; 60ns (java-segment-router, no injects) (let [req (map->Req {:request-method :get, :uri "/user/repos"})] (title "static") (assert (= {:status 200, :body "/user/repos"} (app req))) @@ -321,6 +325,8 @@ ;; 160ns (httprouter) ;; 990ns ;; 830ns (faster decode params) + ;; 560µs (java-segment-router) + ;; 490ns (java-segment-router, no injects) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -329,6 +335,8 @@ ;; 30µs (httprouter) ;; 190µs ;; 160µs (faster decode params) + ;; 120µs (java-segment-router) + ;; 100µs (java-segment-router, no injects) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/impl_perf_test.clj b/perf-test/clj/reitit/impl_perf_test.clj index bce2abc2..89fd604d 100644 --- a/perf-test/clj/reitit/impl_perf_test.clj +++ b/perf-test/clj/reitit/impl_perf_test.clj @@ -185,9 +185,19 @@ :c "1+1" :d "1"})) +(defn split! [] + + (suite "split") + + ;; 114ns (String/split) + ;; 82ns (Trie/split) + (test "Splitting a String") + (test! impl/segments "/olipa/kerran/:avaruus")) + (comment (url-decode!) (url-encode!) (form-decode!) (form-encode!) - (url-encode-coll!)) + (url-encode-coll!) + (split!)) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 62f07928..eb962620 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -432,6 +432,7 @@ ;; 2065ns ;; 662ns (prefix-tree-router) ;; 567ns (segment-router) + ;; 334ns (java-segment-router) (b! "reitit" reitit-f) ;; 2845ns @@ -441,6 +442,7 @@ ;; 702ns (before path-parameters) ;; 806ns (decode path-parameters) ;; 735ns (maybe-map-values) + ;; 487ns (java-segment-router) (b! "reitit-ring" reitit-ring-f) ;; 2821ns diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index 57d78175..5669eb4e 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -69,14 +69,11 @@ (p/insert acc p d)) nil routes)) -#_(def reitit-tree - (reduce - (fn [acc [p d]] - (trie/insert acc p d)) - nil routes)) - (def reitit-segment - (segment/create routes)) + (reduce + (fn [acc [p d]] + (segment/insert acc p d)) + nil routes)) (defn bench! [] diff --git a/perf-test/clj/reitit/ring_perf_test.clj b/perf-test/clj/reitit/ring_perf_test.clj index bc3ac653..b3226120 100644 --- a/perf-test/clj/reitit/ring_perf_test.clj +++ b/perf-test/clj/reitit/ring_perf_test.clj @@ -18,21 +18,31 @@ ;; Memory: 16 GB ;; -(def app +(defn create-app [options] (ring/ring-handler (ring/router [["/auth/login" identity] ["/auth/recovery/token/:token" identity] - ["/workspace/:project/:page" identity]]))) + ["/workspace/:project/:page" identity]]) + (ring/create-default-handler) + options)) -(comment - (let [request {:request-method :post, :uri "/auth/login"}] +(defn bench-app [] + (let [request {:request-method :post, :uri "/auth/login"} + app1 (create-app nil) + app2 (create-app {:inject-match? false, :inject-router? false})] ;; 192ns (initial) ;; 163ns (always assoc path params) ;; 132ns (expand methods) + ;; 111ns (java-segment-router) (cc/quick-bench - (app request)) + (app1 request)) ;; 113ns (don't inject router) ;; 89ns (don't inject router & match) - )) + ;; 77ns (java-segment-router) + (cc/quick-bench + (app2 request)))) + +(comment + (bench-app)) diff --git a/project.clj b/project.clj index 5f4a25af..2970c1ca 100644 --- a/project.clj +++ b/project.clj @@ -60,6 +60,8 @@ "modules/reitit-sieppari/src" "modules/reitit-pedestal/src"] + :java-source-paths ["modules/reitit-core/java-src"] + :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/clojurescript "1.10.439"]