From 2a1fea2ccb9cd736b64b7231579c5ece4d42cce2 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 8 Jan 2019 10:03:06 +0200 Subject: [PATCH 01/30] Initial Java Trie --- modules/reitit-core/java-src/reitit/Trie.java | 148 ++++++++++++++++++ modules/reitit-core/src/reitit/segment.cljc | 17 +- 2 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 modules/reitit-core/java-src/reitit/Trie.java diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java new file mode 100644 index 00000000..2a0219fe --- /dev/null +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -0,0 +1,148 @@ +package reitit; + +import clojure.lang.Keyword; + +import java.util.*; + +import static java.util.Arrays.asList; + +public class Trie { + + public static class Match { + public Map params = new HashMap<>(); + public Object data; + + @Override + public String toString() { + Map m = new HashMap<>(); + m.put(Keyword.intern("data"), data); + m.put(Keyword.intern("params"), params); + return m.toString(); + } + } + + private Map childs = new HashMap<>(); + private Map wilds = new HashMap<>(); + 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; + for (String p : paths) { + if (p.startsWith(":")) { + Keyword k = Keyword.intern(p.substring(1)); + Trie s = pointer.wilds.get(k); + if (s == null) { + s = new Trie(); + pointer.wilds.put(k, s); + } + pointer = s; + } else if (p.startsWith("*")) { + Keyword k = Keyword.intern(p.substring(1)); + Trie s = pointer.catchAll.get(k); + if (s == null) { + s = new Trie(); + pointer.catchAll.put(k, s); + } + break; + } else { + Trie s = pointer.childs.get(p); + if (s == null) { + s = new Trie(); + pointer.childs.put(p, s); + } + pointer = s; + } + } + pointer.data = data; + return this; + } + + public static List split(String path) { + ArrayList strings = new ArrayList<>(asList(path.split("/"))); + strings.remove(0); + return strings; + } + + 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")); + + System.out.println(lookup( + new Trie().add("/user/:id/profile/:type/", 1), + "/user/1/profile/compat")); + + System.out.println(lookup( + new Trie().add("/user/*path", 1), + "/user/1/profile/compat")); + } +} diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index c96de655..98173c16 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -1,7 +1,8 @@ (ns reitit.segment (:refer-clojure :exclude [-lookup]) (:require [reitit.impl :as impl] - [clojure.string :as str])) + [clojure.string :as str]) + #?(:clj (:import (reitit Trie Trie$Match)))) (defrecord Match [data path-params]) @@ -44,13 +45,19 @@ (if catch-all (-catch-all children' catch-all path-params p ps))))))))) (defn insert [root path data] - (-insert (or root (segment)) (impl/segments path) (map->Match {:data data}))) + #?(:cljs (-insert (or root (segment)) (impl/segments path) (map->Match {:data data})) + :clj (.add (or ^Trie root ^Trie (Trie.)) ^String path data))) (defn create [paths] (reduce (fn [segment [p data]] - (insert segment p data)) - nil paths)) + #?(:cljs (insert segment p data) + :clj (.add ^Trie segment ^String p data))) + #?(:cljs nil + :clj (Trie.)) + paths)) (defn lookup [segment path] - (-lookup segment (impl/segments path) {})) + #?(:cljs (-lookup segment (impl/segments path) {}) + :clj (if-let [match ^Trie$Match (Trie/lookup segment path)] + (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) From ae2337621f581e5bf07383fd4b37d7a8a924b53e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 8 Jan 2019 10:03:35 +0200 Subject: [PATCH 02/30] calf --- modules/reitit-core/java-src/reitit/Util.java | 96 ++++++++ perf-test/clj/reitit/calf_perf_test.clj | 218 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 modules/reitit-core/java-src/reitit/Util.java create mode 100644 perf-test/clj/reitit/calf_perf_test.clj diff --git a/modules/reitit-core/java-src/reitit/Util.java b/modules/reitit-core/java-src/reitit/Util.java new file mode 100644 index 00000000..767731c4 --- /dev/null +++ b/modules/reitit-core/java-src/reitit/Util.java @@ -0,0 +1,96 @@ +package reitit; + +import clojure.lang.Keyword; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Util { + + public static Map matchURI(String uri, List patternTokens) { + final MatchResult result = matchURI(uri, 0, patternTokens, false); + return result == null ? null : result.getParams(); + } + + public static MatchResult matchURI(String uri, int beginIndex, List patternTokens) { + return matchURI(uri, beginIndex, patternTokens, false); + } + + /** + * Match a URI against URI-pattern tokens and return match result on successful match, {@code null} otherwise. + * When argument {@code attemptPartialMatch} is {@code true}, both full and partial match are attempted without any + * performance penalty. When argument {@code attemptPartialMatch} is {@code false}, only a full match is attempted. + * + * @param uri the URI string to match + * @param beginIndex beginning index in the URI string to match + * @param patternTokens URI pattern tokens to match the URI against + * @param attemptPartialMatch whether attempt partial match when full match is not possible + * @return a match result on successful match, {@literal null} otherwise + */ + public static MatchResult matchURI(String uri, int beginIndex, List patternTokens, boolean attemptPartialMatch) { + if (beginIndex == MatchResult.FULL_MATCH_INDEX) { // if already a full-match then no need to match further + return MatchResult.NO_MATCH; + } + final int tokenCount = patternTokens.size(); + // if length==1, then token must be string (static URI path) + if (tokenCount == 1) { + final String staticPath = (String) patternTokens.get(0); + if (uri.startsWith(staticPath, beginIndex)) { // URI begins with the path, so at least partial match exists + if ((uri.length() - beginIndex) == staticPath.length()) { // if full match exists, then return as such + return MatchResult.FULL_MATCH_NO_PARAMS; + } + return attemptPartialMatch ? MatchResult.partialMatch(staticPath.length()) : MatchResult.NO_MATCH; + } else { + return MatchResult.NO_MATCH; + } + } + final int uriLength = uri.length(); + final Map pathParams = new HashMap(tokenCount); + int uriIndex = beginIndex; + OUTER: + for (final Object token : patternTokens) { + if (uriIndex >= uriLength) { + return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH; + } + if (token instanceof String) { + final String tokenStr = (String) token; + if (uri.startsWith(tokenStr, uriIndex)) { + uriIndex += tokenStr.length(); // now i==n if last string token + } else { // 'string token mismatch' implies no match + return MatchResult.NO_MATCH; + } + } else { + final StringBuilder sb = new StringBuilder(); + for (int j = uriIndex; j < uriLength; j++) { // capture param chars in one pass + final char ch = uri.charAt(j); + if (ch == '/') { // separator implies we got param value, now continue + pathParams.put(token, sb.toString()); + uriIndex = j; + continue OUTER; + } else { + sb.append(ch); + } + } + // 'separator not found' implies URI has ended + pathParams.put(token, sb.toString()); + uriIndex = uriLength; + } + } + if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match + return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH; + } + return MatchResult.fullMatch(pathParams); + } + + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add("/user/"); + list.add(Keyword.intern("userId")); + list.add("/profile/"); + list.add(Keyword.intern("type")); + list.add("/"); + System.out.println(Util.matchURI("/user/1234/profile/compact/", list)); + } +} diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj new file mode 100644 index 00000000..9b923a46 --- /dev/null +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -0,0 +1,218 @@ +(ns reitit.calf-perf-test + (:require [criterium.core :as cc] + [reitit.perf-utils :refer :all] + [ring.util.codec] + [reitit.impl] + [reitit.segment :as segment] + [reitit.impl :as impl] + [reitit.ring :as ring] + [reitit.core :as r]) + (:import (reitit Trie))) + +;; +;; start repl with `lein perf repl` +;; perf measured with the following setup: +;; +;; Model Name: MacBook Pro +;; Model Identifier: MacBookPro11,3 +;; Processor Name: Intel Core i7 +;; Processor Speed: 2,5 GHz +;; Number of Processors: 1 +;; Total Number of Cores: 4 +;; L2 Cache (per Core): 256 KB +;; L3 Cache: 6 MB +;; Memory: 16 GB +;; + + +(defn test! [f input] + (do + (println "\u001B[33m") + (println (pr-str input) "=>" (pr-str (f input))) + (println "\u001B[0m") + (cc/quick-bench (f input)))) + +(defn h11 [id type] {:status 200 + :headers {"Content-Type" "text/plain"} + :body (str id ".11." type)}) +(defn h12 [id type] {:status 200 + :headers {"Content-Type" "text/plain"} + :body (str id ".12." type)}) +(defn h1x [] {:status 405 + :headers {"Allow" "GET, PUT" + "Content-Type" "text/plain"} + :body "405 Method not supported. Supported methods are: GET, PUT"}) + +(defn h21 [id] {:status 200 + :headers {"Content-Type" "text/plain"} + :body (str id ".21")}) +(defn h22 [id] {:status 200 + :headers {"Content-Type" "text/plain"} + :body (str id ".22")}) +(defn h2x [] {:status 405 + :headers {"Allow" "GET, PUT" + "Content-Type" "text/plain"} + :body "405 Method not supported. Supported methods are: GET, PUT"}) +(defn h30 [cid did] {:status 200 + :headers {"Content-Type" "text/plain"} + :body (str cid ".3." did)}) +(defn h3x [] {:status 405 + :headers {"Allow" "PUT" + "Content-Type" "text/plain"} + :body "405 Method not supported. Only PUT is supported."}) +(defn h40 [] {:status 200 + :headers {"Content-Type" "text/plain"} + :body "4"}) +(defn h4x [] {:status 405 + :headers {"Allow" "PUT" + "Content-Type" "text/plain"} + :body "405 Method not supported. Only PUT is supported."}) +(defn hxx [] {:status 400 + :headers {"Content-Type" "text/plain"} + :body "400 Bad request. URI does not match any available uri-template."}) + +(def handler-reitit + (ring/ring-handler + (ring/router + [["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type)) + :put (fn [{{:keys [id type]} :path-params}] (h12 id type)) + :handler (fn [_] (h1x))}] + #_["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id)) + :put (fn [{{:keys [id]} :path-params}] (h22 id)) + :handler (fn [_] (h2x))}] + #_["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did)) + :handler (fn [_] (h3x))}] + #_["/this/is/a/static/route" {:put (fn [_] (h40)) + :handler (fn [_] (h4x))}]]) + (fn [_] (hxx)))) + +#_(let [request {:request-method :put + :uri "/this/is/a/static/route"}] + (handler-reitit request) + (cc/quick-bench + (handler-reitit request))) + +(let [request {:request-method :get + :uri "/user/1234/profile/compact/"}] + (handler-reitit request) + ;; OLD: 1338ns + ;; NEW: 981ns + #_(cc/quick-bench + (handler-reitit request))) + +(comment + + ;; 849ns (clojure, original) + ;; 599ns (java, initial) + ;; 810ns (linear) + (let [router (r/router ["/user/:id/profile/:type"])] + (cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact"))) + + ;; 131ns + (let [route ["/user/" :id "/profile/" :type "/"]] + (cc/quick-bench + (Util/matchURI "/user/1234/profile/compact/" route))) + + ;; 728ns + (cc/quick-bench + (r/match-by-path ring/ROUTER (:uri ring/REQUEST)))) + +(set! *warn-on-reflection* true) + +(comment + (let [request {:request-method :get + :uri "/user/1234/profile/compact/"}] + (time + (dotimes [_ 1000] + (handler-reitit request))))) + +(import '[reitit Util]) + +#_(cc/quick-bench + (Trie/split "/this/is/a/static/route")) + +(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]) + +(import '[reitit Segment2]) + +(def paths ["kikka" "kukka" "kakka" "abba" "jabba" "1" "2" "3" "4"]) +(def a (Segment2/createArray paths)) +(def h (Segment2/createHash paths)) + +(set! *warn-on-reflection* true) + +(comment + (let [segment (segment/create + [["/user/:id/profile/:type/" 1] + ["/user/:id/permissions/" 2] + ["/company/:cid/dept/:did/" 3] + ["/this/is/a/static/route" 4]])] + (segment/lookup segment "/user/1/profile/compat/") + + ;; OLD: 602ns + ;; NEW: 472ns + (cc/quick-bench + (segment/lookup segment "/user/1/profile/compat/")) + + ;; OLD: 454ns + ;; NEW: 372ns + (cc/quick-bench + (segment/lookup segment "/user/1/permissions/")))) + +#_(cc/quick-bench + (Trie/split "/user/1/profile/compat")) + +#_(Trie/split "/user/1/profile/compat") + +#_(cc/quick-bench + (Segment2/hashLookup h "abba")) + + + +(comment + (cc/quick-bench + (dotimes [_ 1000] + ;; 7ns + (Segment2/arrayLookup a "abba"))) + + (cc/quick-bench + (dotimes [_ 1000] + ;; 3ns + (Segment2/hashLookup h "abba")))) + +(comment + (time + (dotimes [_ 1000] + (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]))) + + + (time + (let [s (s/create [["/user/:id/profile/:type/" 1]])] + (dotimes [_ 1000] + (s/lookup s "/user/1234/profile/compact/")))) + + (let [m {"/abba" 1}] + (time + (dotimes [_ 1000] + (get m "/abba")))) + + (time + (dotimes [_ 1000] + (Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false))) + + ;; 124ns + (cc/quick-bench + (Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false)) + + ;; 166ns + (cc/quick-bench + (impl/segments "/user/1234/profile/compact/")) + + ;; 597ns + (let [s (s/create [["/user/:id/profile/:type/" 1]])] + (cc/quick-bench + (s/lookup s "/user/1234/profile/compact/"))) + + (let [s (s/create [["/user/:id/profile/:type/" 1]])] + (s/lookup s "/user/1234/profile/compact/"))) From e1925c8462666cbe6208b1e993de8ef10ee28793 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 8 Jan 2019 10:03:43 +0200 Subject: [PATCH 03/30] . --- .../java-src/reitit/MatchResult.java | 77 +++++++++++++++ .../reitit-core/java-src/reitit/Segment2.java | 97 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 modules/reitit-core/java-src/reitit/MatchResult.java create mode 100644 modules/reitit-core/java-src/reitit/Segment2.java diff --git a/modules/reitit-core/java-src/reitit/MatchResult.java b/modules/reitit-core/java-src/reitit/MatchResult.java new file mode 100644 index 00000000..56c2e31f --- /dev/null +++ b/modules/reitit-core/java-src/reitit/MatchResult.java @@ -0,0 +1,77 @@ +package reitit; + +import java.util.Collections; +import java.util.Map; + +public class MatchResult { + + public static final MatchResult NO_MATCH = null; + + @SuppressWarnings("unchecked") + public static final Map NO_PARAMS = Collections.EMPTY_MAP; + + public static final int FULL_MATCH_INDEX = -1; + + public static final MatchResult FULL_MATCH_NO_PARAMS = new MatchResult(NO_PARAMS, FULL_MATCH_INDEX); + + private final Map params; + + /** + * End index in the URI when match stopped. -1 implies match fully ended. + */ + private final int endIndex; // excluding + + protected MatchResult(Map params, int endIndex) { + this.params = params; + this.endIndex = endIndex; + } + + // ----- factory methods ----- + + public static MatchResult partialMatch(Map params, int endIndex) { + return new MatchResult(params, endIndex); + } + + public static MatchResult partialMatch(int endIndex) { + return new MatchResult(NO_PARAMS, endIndex); + } + + public static MatchResult fullMatch(Map params) { + return new MatchResult(params, FULL_MATCH_INDEX); + } + + // ----- utility methods ----- + + public Map getParams() { + return params; + } + + public int getEndIndex() { + return endIndex; + } + + public boolean isFullMatch() { + return endIndex == FULL_MATCH_INDEX; + } + + // ----- overridden methods ----- + + @Override + public String toString() { + return String.format("params: %s, endIndex: %d", params, endIndex); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MatchResult) { + MatchResult other = (MatchResult) obj; + return other.params.equals(params) && other.endIndex == endIndex; + } + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} diff --git a/modules/reitit-core/java-src/reitit/Segment2.java b/modules/reitit-core/java-src/reitit/Segment2.java new file mode 100644 index 00000000..b968b547 --- /dev/null +++ b/modules/reitit-core/java-src/reitit/Segment2.java @@ -0,0 +1,97 @@ +package reitit; + +import java.util.*; + +public class Segment2 { + + private List edges = new ArrayList<>(); + private Object data; + + public boolean isLeaf() { + return edges.isEmpty(); + } + + public static class Edge { + String path; + Segment2 segment; + } + + public static Object lookup(Segment2 root, String path) { + Segment2 segment = root; + Integer pathLength = path.length(); + + while (segment != null && !segment.isLeaf()) { + Edge edge = null; + for (Edge e : segment.edges) { + System.out.println("EDGE:" + e.path + "/" + e.segment); + if (path.equals(e.path)) { + edge = e; + break; + } + } + if (edge != null) { + segment = edge.segment; + } else { + return null; + } + } + return segment != null ? segment.data : null; + } + + public static Edge endpoint(String path) { + Edge edge = new Edge(); + edge.path = path; + Segment2 s = new Segment2(); + s.data = path; + edge.segment = s; + return edge; + } + + public static Edge context(String path, Edge... edges) { + Edge edge = new Edge(); + edge.path = path; + Segment2 s = new Segment2(); + s.edges.addAll(Arrays.asList(edges)); + edge.segment = s; + return edge; + } + + public static void main(String[] args) { + Segment2 root = new Segment2(); + + root.edges.add(endpoint("/kikka")); + root.edges.add(endpoint("/kukka")); + root.edges.add( + context("/api", + endpoint("/ping"), + endpoint("pong"))); + System.out.println(lookup(root, "/api/ping")); + } + + public static Map createHash(List paths) { + Map m = new HashMap<>(); + for (String p : paths) { + m.put(p, p); + } + return m; + } + + public static List createArray(List paths) { + return new ArrayList<>(paths); + } + + public static Object hashLookup(Map m, String path) { + return m.get(path); + } + + public static Object arrayLookup(ArrayList paths, String path) { + Object data = null; + for (String p : paths) { + if (p.equals(path)) { + data = path; + break; + } + } + return data; + } +} From 3f86e24b89e5418eab7ee4cb94a8c6a22a0edadf Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 9 Jan 2019 08:04:30 +0200 Subject: [PATCH 04/30] faster split --- modules/reitit-core/java-src/reitit/Trie.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 2a0219fe..88186d2b 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -8,6 +8,21 @@ import static java.util.Arrays.asList; public class Trie { + public static ArrayList split(final String path) { + final ArrayList segments = new ArrayList<>(4); + final int size = path.length(); + int start = 1; + for (int i = start; i < size; i++) { + final char c = path.charAt(i); + if (c == '/') { + segments.add(path.substring(start, i)); + start = i + 1; + } + } + segments.add(path.substring(start, size)); + return segments; + } + public static class Match { public Map params = new HashMap<>(); public Object data; @@ -112,10 +127,6 @@ public class Trie { return this; } - public static List split(String path) { - ArrayList strings = new ArrayList<>(asList(path.split("/"))); - strings.remove(0); - return strings; } public static void main(String[] args) { From 80dea6cfefba2f4c8cec7920c783bc35fb376bdd Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 14:03:07 +0200 Subject: [PATCH 05/30] Snappier Trie --- modules/reitit-core/java-src/reitit/Trie.java | 331 +++++++++++++----- modules/reitit-core/project.clj | 1 + modules/reitit-core/src/reitit/core.cljc | 5 +- modules/reitit-core/src/reitit/impl.cljc | 11 +- modules/reitit-core/src/reitit/segment.cljc | 19 +- perf-test/clj/reitit/bide_perf_test.clj | 14 +- perf-test/clj/reitit/go_perf_test.clj | 10 +- perf-test/clj/reitit/impl_perf_test.clj | 12 +- .../clj/reitit/opensensors_perf_test.clj | 2 + .../clj/reitit/prefix_tree_perf_test.clj | 11 +- perf-test/clj/reitit/ring_perf_test.clj | 22 +- project.clj | 2 + 12 files changed, 323 insertions(+), 117 deletions(-) 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"] From 75c4f78f5d3d0db341f75bab88fec7f0ed52f6c8 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 14:04:24 +0200 Subject: [PATCH 06/30] :inject-router? and :inject-match? for ring & http --- modules/reitit-http/src/reitit/http.cljc | 52 +++++---- modules/reitit-ring/src/reitit/ring.cljc | 58 +++++++--- perf-test/clj/reitit/calf_perf_test.clj | 138 ++++++++++++++++++++--- 3 files changed, 193 insertions(+), 55 deletions(-) diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc index cb694b49..75eda84d 100644 --- a/modules/reitit-http/src/reitit/http.cljc +++ b/modules/reitit-http/src/reitit/http.cljc @@ -73,12 +73,22 @@ (r/router data opts)))) (defn routing-interceptor - "A Pedestal-style routing interceptor that enqueus the interceptors into context." - [router default-handler {:keys [interceptors executor]}] + "Creates a Pedestal-style routing interceptor that enqueus the interceptors into context. + Takes http-router, default ring-handler and and options map, with the following keys: + + | key | description | + | ------------------|-------------| + | `:executor` | `reitit.interceptor.Executor` for the interceptor chain + | `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler + | `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true) + | `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)" + [router default-handler {:keys [interceptors executor inject-match? inject-router?] + :or {inject-match? true, inject-router? true}}] (let [default-handler (or default-handler (fn ([_]))) default-interceptors (->> interceptors (map #(interceptor/into-interceptor % nil (r/options router)))) - default-queue (interceptor/queue executor default-interceptors)] + default-queue (interceptor/queue executor default-interceptors) + enrich-request (ring/create-enrich-request inject-match? inject-router?)] {:name ::router :enter (fn [{:keys [request] :as context}] (if-let [match (r/match-by-path router (:uri request))] @@ -86,10 +96,7 @@ path-params (:path-params match) endpoint (-> match :result method) interceptors (or (:queue endpoint) (:interceptors endpoint)) - request (-> request - (impl/fast-assoc :path-params path-params) - (impl/fast-assoc ::r/match match) - (impl/fast-assoc ::r/router router)) + request (enrich-request request path-params match router) context (assoc context :request request) queue (interceptor/queue executor (concat default-interceptors interceptors))] (interceptor/enqueue executor context queue)) @@ -103,13 +110,16 @@ "Creates a ring-handler out of a http-router, optional default ring-handler and options map, with the following keys: - | key | description | - | ----------------|-------------| - | `:executor` | `reitit.interceptor.Executor` for the interceptor chain - | `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler" + | key | description | + | ------------------|-------------| + | `:executor` | `reitit.interceptor.Executor` for the interceptor chain + | `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler + | `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true) + | `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)" ([router opts] (ring-handler router nil opts)) - ([router default-handler {:keys [executor interceptors]}] + ([router default-handler {:keys [executor interceptors inject-match? inject-router?] + :or {inject-match? true, inject-router? true}}] (let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil)))) default-queue (->> [default-handler] (concat interceptors) @@ -120,7 +130,9 @@ (dissoc :data) ; data is already merged into routes (cond-> (seq interceptors) (update-in [:data :interceptors] (partial into (vec interceptors))))) - router (reitit.http/router (r/routes router) router-opts)] + router (reitit.http/router (r/routes router) router-opts) + enrich-request (ring/create-enrich-request inject-match? inject-router?) + enrich-default-request (ring/create-enrich-default-request inject-router?)] (with-meta (fn ([request] @@ -129,13 +141,10 @@ path-params (:path-params match) endpoint (-> match :result method) interceptors (or (:queue endpoint) (:interceptors endpoint)) - request (-> request - (impl/fast-assoc :path-params path-params) - (impl/fast-assoc ::r/match match) - (impl/fast-assoc ::r/router router))] + request (enrich-request request path-params match router)] (or (interceptor/execute executor interceptors request) (interceptor/execute executor default-queue request))) - (interceptor/execute executor default-queue (impl/fast-assoc request ::r/router router)))) + (interceptor/execute executor default-queue (enrich-default-request request)))) ([request respond raise] (let [default #(interceptor/execute executor default-queue % respond raise)] (if-let [match (r/match-by-path router (:uri request))] @@ -143,10 +152,7 @@ path-params (:path-params match) endpoint (-> match :result method) interceptors (or (:queue endpoint) (:interceptors endpoint)) - request (-> request - (impl/fast-assoc :path-params path-params) - (impl/fast-assoc ::r/match match) - (impl/fast-assoc ::r/router router)) + request (enrich-request request path-params match router) respond' (fn [response] (if response (respond response) @@ -154,7 +160,7 @@ (if interceptors (interceptor/execute executor interceptors request respond' raise) (default request))) - (default (impl/fast-assoc request ::r/router router)))) + (default (enrich-default-request request)))) nil)) {::r/router router})))) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 42ebcd4f..a0913c2f 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -226,20 +226,54 @@ (not-found-handler request)))))] (create handler))))) +(defn create-enrich-request [inject-match? inject-router?] + (cond + (and inject-match? inject-router?) + (fn enrich-request [request path-params match router] + (-> request + (impl/fast-assoc :path-params path-params) + (impl/fast-assoc ::r/match match) + (impl/fast-assoc ::r/router router))) + inject-router? + (fn enrich-request [request path-params _ router] + (-> request + (impl/fast-assoc :path-params path-params) + (impl/fast-assoc ::r/router router))) + inject-match? + (fn enrich-request [request path-params match _] + (-> request + (impl/fast-assoc :path-params path-params) + (impl/fast-assoc ::r/match match))) + :else + (fn enrich-request [request path-params _ _] + (-> request + (impl/fast-assoc :path-params path-params))))) + +(defn create-enrich-default-request [inject-router?] + (if inject-router? + (fn enrich-request [request router] + (impl/fast-assoc request ::r/router router)) + identity)) + (defn ring-handler "Creates a ring-handler out of a router, optional default ring-handler and options map, with the following keys: - | key | description | - | --------------|-------------| - | `:middleware` | Optional sequence of middleware that wrap the ring-handler" + | key | description | + | ------------------|-------------| + | `:middleware` | Optional sequence of middleware that wrap the ring-handler + | `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true) + | `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)" ([router] (ring-handler router nil)) ([router default-handler] (ring-handler router default-handler nil)) - ([router default-handler {:keys [middleware]}] + ([router default-handler {:keys [middleware inject-match? inject-router?] + :or {inject-match? true, inject-router? true}}] (let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil)))) - wrap (if middleware (partial middleware/chain middleware) identity)] + wrap (if middleware (partial middleware/chain middleware) identity) + enrich-request (create-enrich-request inject-match? inject-router?) + enrich-default-request (create-enrich-default-request inject-router?)] (with-meta (wrap (fn @@ -249,24 +283,18 @@ path-params (:path-params match) result (:result match) handler (-> result method :handler (or default-handler)) - request (-> request - (impl/fast-assoc :path-params path-params) - (impl/fast-assoc ::r/match match) - (impl/fast-assoc ::r/router router))] + request (enrich-request request path-params match router)] (or (handler request) (default-handler request))) - (default-handler (impl/fast-assoc request ::r/router router)))) + (default-handler (enrich-default-request request)))) ([request respond raise] (if-let [match (r/match-by-path router (:uri request))] (let [method (:request-method request) path-params (:path-params match) result (:result match) handler (-> result method :handler (or default-handler)) - request (-> request - (impl/fast-assoc :path-params path-params) - (impl/fast-assoc ::r/match match) - (impl/fast-assoc ::r/router router))] + request (enrich-request request path-params match router)] ((routes handler default-handler) request respond raise)) - (default-handler (impl/fast-assoc request ::r/router router) respond raise)) + (default-handler (enrich-default-request request) respond raise)) nil))) {::r/router router})))) diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 9b923a46..5137b088 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -7,7 +7,7 @@ [reitit.impl :as impl] [reitit.ring :as ring] [reitit.core :as r]) - (:import (reitit Trie))) + (:import (reitit Trie Trie$Matcher))) ;; ;; start repl with `lein perf repl` @@ -77,35 +77,133 @@ [["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type)) :put (fn [{{:keys [id type]} :path-params}] (h12 id type)) :handler (fn [_] (h1x))}] - #_["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id)) + ["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id)) :put (fn [{{:keys [id]} :path-params}] (h22 id)) :handler (fn [_] (h2x))}] - #_["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did)) + ["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did)) :handler (fn [_] (h3x))}] - #_["/this/is/a/static/route" {:put (fn [_] (h40)) + ["/this/is/a/static/route" {:put (fn [_] (h40)) :handler (fn [_] (h4x))}]]) (fn [_] (hxx)))) #_(let [request {:request-method :put - :uri "/this/is/a/static/route"}] - (handler-reitit request) - (cc/quick-bench - (handler-reitit request))) + :uri "/this/is/a/static/route"}] + (handler-reitit request) + (cc/quick-bench + (handler-reitit request))) (let [request {:request-method :get :uri "/user/1234/profile/compact/"}] - (handler-reitit request) - ;; OLD: 1338ns - ;; NEW: 981ns + ;; OLD: 1338ns + ;; NEW: 981ns + ;; JAVA: 805ns + ;; NO-INJECT: 704ns #_(cc/quick-bench - (handler-reitit request))) + (handler-reitit request)) + (handler-reitit request)) + +(comment + (impl/segments "/user/1234/profile/compact") + ;; 145ns + (cc/quick-bench + (impl/segments "/user/1234/profile/compact"))) + +(comment + (Trie/split "/user/1234/profile/compact") + ;; 91ns + (cc/quick-bench + (Trie/split "/user/1234/profile/compact"))) + +(comment + (let [router (r/router ["/user/:id/profile/:type"])] + (cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact")))) + +(let [lookup ^Trie$Matcher (Trie/sample)] + (Trie/lookup lookup "/user/1234/profile/compact") + #_(cc/quick-bench + (Trie/lookup lookup "/user/1234/profile/compact"))) + +(let [router (r/router [["/user/:id" ::1] + ["/user/:id/permissions" ::2] + ["/company/:cid/dept/:did" ::3] + ["/this/is/a/static/route" ::4]])] + #_(cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact")) + (r/match-by-path router "/user/1234")) + +;; 281ns +(let [router (r/router [["/user/:id/profile/:type" ::1] + ["/user/:id/permissions" ::2] + ["/company/:cid/dept/:did" ::3] + ["/this/is/a/static/route" ::4]])] + #_(cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact")) + (r/match-by-path router "/user/1234/profile/compact")) + +(read-string + (str + (.matcher + (doto (Trie.) + (.add "/user" 1) + #_(.add "/user/id/permissions" 2) + (.add "/user/id/permissions2" 3))))) + +(Trie/lookup + (.matcher + (doto (Trie.) + (.add "/user/1" 1) + (.add "/user/1/permissions" 2))) + "/user/1") + +(.matcher + (doto (Trie.) + (.add "/user/1" 1) + (.add "/user/1/permissions" 2))) + +;; 137ns +(let [m (.matcher + (doto (Trie.) + (.add "/user/:id/profile/:type" 1)))] + #_(cc/quick-bench + (Trie/lookup m "/user/1234/profile/compact")) + (Trie/lookup m "/user/1234/profile/compact")) (comment + (let [matcher ^Trie$Matcher (Trie/sample)] + (Trie/lookup matcher "/user/1234/profile/compact") + (cc/quick-bench + (Trie/lookup matcher "/user/1234/profile/compact"))) + + ;; 173ns + (let [lookup ^Trie$Matcher (Trie/tree2)] + (Trie/lookup lookup "/user/1234/profile/compact") + (cc/quick-bench + (Trie/lookup lookup "/user/1234/profile/compact"))) + + + ;; 140ns + (let [lookup ^Trie$Matcher (Trie/tree1)] + (Trie/lookup lookup "/user/1234/profile/compact") + (cc/quick-bench + (Trie/lookup lookup "/user/1234/profile/compact"))) + ;; 849ns (clojure, original) ;; 599ns (java, initial) - ;; 810ns (linear) + ;; 173ns (fast split) (let [router (r/router ["/user/:id/profile/:type"])] + (r/match-by-path router "/user/1234/profile/compact") + (cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact"))) + + ;; 849ns (clojure, original) + ;; 599ns (java, initial) + ;; 173ns (java, optimized) + (let [router (r/router [["/user/:id/profile/:type/" ::1] + ["/user/:id/permissions/" ::2] + ["/company/:cid/dept/:did/" ::3] + ["/this/is/a/static/route" ::4]])] (cc/quick-bench (r/match-by-path router "/user/1234/profile/compact"))) @@ -129,10 +227,16 @@ (import '[reitit Util]) -#_(cc/quick-bench - (Trie/split "/this/is/a/static/route")) +(comment + (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]) + (cc/quick-bench + (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])) -(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]) + (cc/quick-bench + (Trie/split "/user/1234/profile/compact/")) + + (cc/quick-bench + (.split "/user/1234/profile/compact/" "/" 666))) (import '[reitit Segment2]) @@ -161,7 +265,7 @@ (segment/lookup segment "/user/1/permissions/")))) #_(cc/quick-bench - (Trie/split "/user/1/profile/compat")) + (Trie/split "/user/1/profile/compat")) #_(Trie/split "/user/1/profile/compat") From e619234e84933f195c2eeb3d286c66e2dc650137 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 16:41:03 +0200 Subject: [PATCH 07/30] Test against calfpath --- .../clj/reitit/opensensors_perf_test.clj | 75 ++++++++++++++++++- project.clj | 1 + 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index eb962620..7e39bdc1 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -8,6 +8,7 @@ [bidi.bidi :as bidi] [ataraxy.core :as ataraxy] [compojure.core :refer [routes context ANY]] + [calfpath.core :as cp] [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] @@ -366,6 +367,68 @@ ["/v1/users/:user-id/bookmarks" :get handler :route-name :test/route56] ["/v1/orgs/:org-id/topics" :get handler :route-name :test/route57]]))) +(defn opensensors-calfpath-handler [request] + (cp/->uri + request + "/v2/whoami" [] (cp/->get request (handler request) nil) + "/v2/users/:user-id/datasets" [] (cp/->get request (handler request) nil) + "/v2/public/projects/:project-id/datasets" [] (cp/->get request (handler request) nil) + "/v1/public/topics/:topic" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/orgs/:org-id" [] (cp/->get request (handler request) nil) + "/v1/search/topics/:term" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/invitations" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/devices/:batch/:type" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/topics" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/bookmarks/followers" [] (cp/->get request (handler request) nil) + "/v2/datasets/:dataset-id" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/usage-stats" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/devices/:client-id" [] (cp/->get request (handler request) nil) + "/v1/messages/user/:user-id" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/devices" [] (cp/->get request (handler request) nil) + "/v1/public/users/:user-id" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/errors" [] (cp/->get request (handler request) nil) + "/v1/public/orgs/:org-id" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/invitations" [] (cp/->get request (handler request) nil) + "/v2/public/messages/dataset/bulk" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/devices/bulk" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/device-errors" [] (cp/->get request (handler request) nil) + "/v2/login" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/usage-stats" [] (cp/->get request (handler request) nil) + "/v2/users/:user-id/devices" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/claim-device/:client-id" [] (cp/->get request (handler request) nil) + "/v2/public/projects/:project-id" [] (cp/->get request (handler request) nil) + "/v2/public/datasets/:dataset-id" [] (cp/->get request (handler request) nil) + "/v2/users/:user-id/topics/bulk" [] (cp/->get request (handler request) nil) + "/v1/messages/device/:client-id" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/owned-orgs" [] (cp/->get request (handler request) nil) + "/v1/topics/:topic" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/bookmark/:topic" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/members/:user-id" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/devices/:client-id" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/devices" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/members" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/members/invitation-data/:user-id" [] (cp/->get request (handler request) nil) + "/v2/orgs/:org-id/topics" [] (cp/->get request (handler request) nil) + "/v1/whoami" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/api-key" [] (cp/->get request (handler request) nil) + "/v2/schemas" [] (cp/->get request (handler request) nil) + "/v2/users/:user-id/topics" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/confirm-membership/:token" [] (cp/->get request (handler request) nil) + "/v2/topics/:topic" [] (cp/->get request (handler request) nil) + "/v1/messages/topic/:topic" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/devices/:client-id/reset-password" [] (cp/->get request (handler request) nil) + "/v2/topics" [] (cp/->get request (handler request) nil) + "/v1/login" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/orgs" [] (cp/->get request (handler request) nil) + "/v2/public/messages/dataset/:dataset-id" [] (cp/->get request (handler request) nil) + "/v1/topics" [] (cp/->get request (handler request) nil) + "/v1/orgs" [] (cp/->get request (handler request) nil) + "/v1/users/:user-id/bookmarks" [] (cp/->get request (handler request) nil) + "/v1/orgs/:org-id/topics" [] (cp/->get request (handler request) nil) + nil)) + (comment (pedestal/find-route (map-tree/router @@ -422,7 +485,9 @@ router (reitit/router routes) reitit-f #(reitit/match-by-path router (:uri %)) reitit-ring-f (ring/ring-handler (ring/router opensensors-routes)) + reitit-ring-fast-f (ring/ring-handler (ring/router opensensors-routes) nil {:inject-router? false, :inject-match? false}) bidi-f #(bidi/match-route opensensors-bidi-routes (:uri %)) + calfpath-f opensensors-calfpath-handler ataraxy-f (partial ataraxy/matches opensensors-ataraxy-routes) compojure-f opensensors-compojure-routes pedestal-f (partial pedestal/find-route opensensors-pedestal-routes) @@ -432,7 +497,7 @@ ;; 2065ns ;; 662ns (prefix-tree-router) ;; 567ns (segment-router) - ;; 334ns (java-segment-router) + ;; 326ns (java-segment-router) (b! "reitit" reitit-f) ;; 2845ns @@ -442,9 +507,15 @@ ;; 702ns (before path-parameters) ;; 806ns (decode path-parameters) ;; 735ns (maybe-map-values) - ;; 487ns (java-segment-router) + ;; 474ns (java-segment-router) (b! "reitit-ring" reitit-ring-f) + ;; 385ns (java-segment-router, no injects) + (b! "reitit-ring-fast" reitit-ring-fast-f) + + ;; 474ns (macros) + (b! "calfpath" calfpath-f) + ;; 2821ns (b! "pedestal" pedestal-f) diff --git a/project.clj b/project.clj index 2970c1ca..65ca3636 100644 --- a/project.clj +++ b/project.clj @@ -101,6 +101,7 @@ [ikitommi/immutant-web "3.0.0-alpha1"] [io.pedestal/pedestal.service "0.5.5"] [io.pedestal/pedestal.jetty "0.5.5"] + [calfpath "0.7.1"] [org.clojure/core.async "0.4.490"] [manifold "0.1.8"] [funcool/promesa "1.9.0"] From 19213dcba7f6d40d9b1f10a2e2e9a66e8d296b62 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 16:41:18 +0200 Subject: [PATCH 08/30] prefix-trie is faster now --- .../clj/reitit/prefix_tree_perf_test.clj | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index 5669eb4e..c507e9dc 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -2,7 +2,8 @@ (:require [clojure.test :refer :all] [io.pedestal.http.route.prefix-tree :as p] [reitit.segment :as segment] - [criterium.core :as cc])) + [criterium.core :as cc]) + (:import (reitit Trie))) ;; ;; testing @@ -69,11 +70,13 @@ (p/insert acc p d)) nil routes)) -(def reitit-segment - (reduce - (fn [acc [p d]] - (segment/insert acc p d)) - nil routes)) +(def matcher + (.matcher + ^Trie + (reduce + (fn [acc [p d]] + (segment/insert acc p d)) + nil routes))) (defn bench! [] @@ -105,8 +108,9 @@ ;; 1.0µs (Match records) ;; 0.63µs (Single sweep path paraµs) ;; 0.51µs (Cleanup) + ;; 0.33µs (Java) (cc/quick-bench - (segment/lookup reitit-segment "/v1/orgs/1/topics"))) + (segment/lookup matcher "/v1/orgs/1/topics"))) (comment (bench!)) @@ -114,4 +118,4 @@ (comment (p/lookup pedestal-tree "/v1/orgs/1/topics") #_(trie/lookup reitit-tree "/v1/orgs/1/topics" {}) - (segment/lookup reitit-segment "/v1/orgs/1/topics")) + (segment/lookup matcher "/v1/orgs/1/topics")) From 54aded44421ef7d78784b9e17de473b3d6ab3f98 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 16:44:59 +0200 Subject: [PATCH 09/30] Dead code --- .../java-src/reitit/MatchResult.java | 77 --------------- .../reitit-core/java-src/reitit/Segment2.java | 97 ------------------- .../reitit/{Trie.java => SegmentTrie.java} | 0 modules/reitit-core/java-src/reitit/Util.java | 96 ------------------ 4 files changed, 270 deletions(-) delete mode 100644 modules/reitit-core/java-src/reitit/MatchResult.java delete mode 100644 modules/reitit-core/java-src/reitit/Segment2.java rename modules/reitit-core/java-src/reitit/{Trie.java => SegmentTrie.java} (100%) delete mode 100644 modules/reitit-core/java-src/reitit/Util.java diff --git a/modules/reitit-core/java-src/reitit/MatchResult.java b/modules/reitit-core/java-src/reitit/MatchResult.java deleted file mode 100644 index 56c2e31f..00000000 --- a/modules/reitit-core/java-src/reitit/MatchResult.java +++ /dev/null @@ -1,77 +0,0 @@ -package reitit; - -import java.util.Collections; -import java.util.Map; - -public class MatchResult { - - public static final MatchResult NO_MATCH = null; - - @SuppressWarnings("unchecked") - public static final Map NO_PARAMS = Collections.EMPTY_MAP; - - public static final int FULL_MATCH_INDEX = -1; - - public static final MatchResult FULL_MATCH_NO_PARAMS = new MatchResult(NO_PARAMS, FULL_MATCH_INDEX); - - private final Map params; - - /** - * End index in the URI when match stopped. -1 implies match fully ended. - */ - private final int endIndex; // excluding - - protected MatchResult(Map params, int endIndex) { - this.params = params; - this.endIndex = endIndex; - } - - // ----- factory methods ----- - - public static MatchResult partialMatch(Map params, int endIndex) { - return new MatchResult(params, endIndex); - } - - public static MatchResult partialMatch(int endIndex) { - return new MatchResult(NO_PARAMS, endIndex); - } - - public static MatchResult fullMatch(Map params) { - return new MatchResult(params, FULL_MATCH_INDEX); - } - - // ----- utility methods ----- - - public Map getParams() { - return params; - } - - public int getEndIndex() { - return endIndex; - } - - public boolean isFullMatch() { - return endIndex == FULL_MATCH_INDEX; - } - - // ----- overridden methods ----- - - @Override - public String toString() { - return String.format("params: %s, endIndex: %d", params, endIndex); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof MatchResult) { - MatchResult other = (MatchResult) obj; - return other.params.equals(params) && other.endIndex == endIndex; - } - return false; - } - - @Override - public int hashCode() { - return toString().hashCode(); - } -} diff --git a/modules/reitit-core/java-src/reitit/Segment2.java b/modules/reitit-core/java-src/reitit/Segment2.java deleted file mode 100644 index b968b547..00000000 --- a/modules/reitit-core/java-src/reitit/Segment2.java +++ /dev/null @@ -1,97 +0,0 @@ -package reitit; - -import java.util.*; - -public class Segment2 { - - private List edges = new ArrayList<>(); - private Object data; - - public boolean isLeaf() { - return edges.isEmpty(); - } - - public static class Edge { - String path; - Segment2 segment; - } - - public static Object lookup(Segment2 root, String path) { - Segment2 segment = root; - Integer pathLength = path.length(); - - while (segment != null && !segment.isLeaf()) { - Edge edge = null; - for (Edge e : segment.edges) { - System.out.println("EDGE:" + e.path + "/" + e.segment); - if (path.equals(e.path)) { - edge = e; - break; - } - } - if (edge != null) { - segment = edge.segment; - } else { - return null; - } - } - return segment != null ? segment.data : null; - } - - public static Edge endpoint(String path) { - Edge edge = new Edge(); - edge.path = path; - Segment2 s = new Segment2(); - s.data = path; - edge.segment = s; - return edge; - } - - public static Edge context(String path, Edge... edges) { - Edge edge = new Edge(); - edge.path = path; - Segment2 s = new Segment2(); - s.edges.addAll(Arrays.asList(edges)); - edge.segment = s; - return edge; - } - - public static void main(String[] args) { - Segment2 root = new Segment2(); - - root.edges.add(endpoint("/kikka")); - root.edges.add(endpoint("/kukka")); - root.edges.add( - context("/api", - endpoint("/ping"), - endpoint("pong"))); - System.out.println(lookup(root, "/api/ping")); - } - - public static Map createHash(List paths) { - Map m = new HashMap<>(); - for (String p : paths) { - m.put(p, p); - } - return m; - } - - public static List createArray(List paths) { - return new ArrayList<>(paths); - } - - public static Object hashLookup(Map m, String path) { - return m.get(path); - } - - public static Object arrayLookup(ArrayList paths, String path) { - Object data = null; - for (String p : paths) { - if (p.equals(path)) { - data = path; - break; - } - } - return data; - } -} diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java similarity index 100% rename from modules/reitit-core/java-src/reitit/Trie.java rename to modules/reitit-core/java-src/reitit/SegmentTrie.java diff --git a/modules/reitit-core/java-src/reitit/Util.java b/modules/reitit-core/java-src/reitit/Util.java deleted file mode 100644 index 767731c4..00000000 --- a/modules/reitit-core/java-src/reitit/Util.java +++ /dev/null @@ -1,96 +0,0 @@ -package reitit; - -import clojure.lang.Keyword; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class Util { - - public static Map matchURI(String uri, List patternTokens) { - final MatchResult result = matchURI(uri, 0, patternTokens, false); - return result == null ? null : result.getParams(); - } - - public static MatchResult matchURI(String uri, int beginIndex, List patternTokens) { - return matchURI(uri, beginIndex, patternTokens, false); - } - - /** - * Match a URI against URI-pattern tokens and return match result on successful match, {@code null} otherwise. - * When argument {@code attemptPartialMatch} is {@code true}, both full and partial match are attempted without any - * performance penalty. When argument {@code attemptPartialMatch} is {@code false}, only a full match is attempted. - * - * @param uri the URI string to match - * @param beginIndex beginning index in the URI string to match - * @param patternTokens URI pattern tokens to match the URI against - * @param attemptPartialMatch whether attempt partial match when full match is not possible - * @return a match result on successful match, {@literal null} otherwise - */ - public static MatchResult matchURI(String uri, int beginIndex, List patternTokens, boolean attemptPartialMatch) { - if (beginIndex == MatchResult.FULL_MATCH_INDEX) { // if already a full-match then no need to match further - return MatchResult.NO_MATCH; - } - final int tokenCount = patternTokens.size(); - // if length==1, then token must be string (static URI path) - if (tokenCount == 1) { - final String staticPath = (String) patternTokens.get(0); - if (uri.startsWith(staticPath, beginIndex)) { // URI begins with the path, so at least partial match exists - if ((uri.length() - beginIndex) == staticPath.length()) { // if full match exists, then return as such - return MatchResult.FULL_MATCH_NO_PARAMS; - } - return attemptPartialMatch ? MatchResult.partialMatch(staticPath.length()) : MatchResult.NO_MATCH; - } else { - return MatchResult.NO_MATCH; - } - } - final int uriLength = uri.length(); - final Map pathParams = new HashMap(tokenCount); - int uriIndex = beginIndex; - OUTER: - for (final Object token : patternTokens) { - if (uriIndex >= uriLength) { - return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH; - } - if (token instanceof String) { - final String tokenStr = (String) token; - if (uri.startsWith(tokenStr, uriIndex)) { - uriIndex += tokenStr.length(); // now i==n if last string token - } else { // 'string token mismatch' implies no match - return MatchResult.NO_MATCH; - } - } else { - final StringBuilder sb = new StringBuilder(); - for (int j = uriIndex; j < uriLength; j++) { // capture param chars in one pass - final char ch = uri.charAt(j); - if (ch == '/') { // separator implies we got param value, now continue - pathParams.put(token, sb.toString()); - uriIndex = j; - continue OUTER; - } else { - sb.append(ch); - } - } - // 'separator not found' implies URI has ended - pathParams.put(token, sb.toString()); - uriIndex = uriLength; - } - } - if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match - return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH; - } - return MatchResult.fullMatch(pathParams); - } - - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add("/user/"); - list.add(Keyword.intern("userId")); - list.add("/profile/"); - list.add(Keyword.intern("type")); - list.add("/"); - System.out.println(Util.matchURI("/user/1234/profile/compact/", list)); - } -} From 93bcc5dad8ea542f8d7a89c88e94b8d2d814f455 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 16:45:43 +0200 Subject: [PATCH 10/30] Trie -> SegmentTrie --- .../java-src/reitit/SegmentTrie.java | 32 +++++------ modules/reitit-core/src/reitit/impl.cljc | 4 +- modules/reitit-core/src/reitit/segment.cljc | 8 +-- perf-test/clj/reitit/calf_perf_test.clj | 53 +++++++++---------- perf-test/clj/reitit/impl_perf_test.clj | 2 +- .../clj/reitit/prefix_tree_perf_test.clj | 4 +- 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 47bf2e91..3cb58981 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -6,7 +6,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.*; -public class Trie { +public class SegmentTrie { public static ArrayList split(final String path) { final ArrayList segments = new ArrayList<>(4); @@ -50,35 +50,35 @@ public class Trie { } } - private Map childs = new HashMap<>(); - private Map wilds = new HashMap<>(); - private Map catchAll = new HashMap<>(); + private Map childs = new HashMap<>(); + private Map wilds = new HashMap<>(); + private Map catchAll = new HashMap<>(); private Object data; - public Trie add(String path, Object data) { + public SegmentTrie add(String path, Object data) { List paths = split(path); - Trie pointer = this; + SegmentTrie pointer = this; for (String p : paths) { if (p.startsWith(":")) { Keyword k = Keyword.intern(p.substring(1)); - Trie s = pointer.wilds.get(k); + SegmentTrie s = pointer.wilds.get(k); if (s == null) { - s = new Trie(); + s = new SegmentTrie(); pointer.wilds.put(k, s); } pointer = s; } else if (p.startsWith("*")) { Keyword k = Keyword.intern(p.substring(1)); - Trie s = pointer.catchAll.get(k); + SegmentTrie s = pointer.catchAll.get(k); if (s == null) { - s = new Trie(); + s = new SegmentTrie(); pointer.catchAll.put(k, s); } break; } else { - Trie s = pointer.childs.get(p); + SegmentTrie s = pointer.childs.get(p); if (s == null) { - s = new Trie(); + s = new SegmentTrie(); pointer.childs.put(p, s); } pointer = s; @@ -93,7 +93,7 @@ public class Trie { return new StaticMatcher(childs.keySet().iterator().next(), childs.values().iterator().next().matcher()); } else { Map m = new HashMap<>(); - for (Map.Entry e : childs.entrySet()) { + for (Map.Entry e : childs.entrySet()) { m.put(e.getKey(), e.getValue().matcher()); } return new StaticMapMatcher(m); @@ -112,7 +112,7 @@ public class Trie { if (!childs.isEmpty()) { matchers.add(staticMatcher()); } - for (Map.Entry e : wilds.entrySet()) { + for (Map.Entry e : wilds.entrySet()) { matchers.add(new WildMatcher(e.getKey(), e.getValue().matcher())); } m = new LinearMatcher(matchers); @@ -301,7 +301,7 @@ public class Trie { public static void main(String[] args) { - Trie trie = new Trie(); + SegmentTrie trie = new SegmentTrie(); //trie.add("/kikka/:id/permissions", 1); trie.add("/kikka/:id", 2); trie.add("/kakka/ping", 3); @@ -311,7 +311,7 @@ public class Trie { System.out.println(lookup(m, "/kikka/1")); /* - Trie trie = new Trie(); + SegmentTrie trie = new SegmentTrie(); trie.add("/user/:id/profile/:type", 1); trie.add("/user/:id/permissions", 2); trie.add("/company/:cid/dept/:did", 3); diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index a4317999..e996def8 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -6,7 +6,7 @@ (:import (java.util.regex Pattern) (java.util HashMap Map) (java.net URLEncoder URLDecoder) - (reitit Trie)))) + (reitit SegmentTrie)))) (defn maybe-map-values "Applies a function to every value of a map, updates the value if not nil. @@ -21,7 +21,7 @@ coll)) (defn segments [path] - #?(:clj (Trie/split ^String path) + #?(:clj (SegmentTrie/split ^String path) :cljs (.split path #"/" 666))) ;; diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index 18a5638d..b0ff46fa 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -2,7 +2,7 @@ (:refer-clojure :exclude [-lookup compile]) (:require [reitit.impl :as impl] [clojure.string :as str]) - #?(:clj (:import (reitit Trie Trie$Match)))) + #?(:clj (:import (reitit SegmentTrie Trie$Match)))) (defrecord Match [data path-params]) @@ -50,13 +50,13 @@ (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 ^SegmentTrie root ^SegmentTrie (SegmentTrie.)) ^String path data))) (defn compile [segment] #?(:cljs segment - :clj (.matcher ^Trie segment))) + :clj (.matcher ^SegmentTrie segment))) (defn lookup [segment path] #?(:cljs (-lookup segment (impl/segments path) {}) - :clj (if-let [match ^Trie$Match (Trie/lookup segment path)] + :clj (if-let [match ^Trie$Match (SegmentTrie/lookup segment path)] (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 5137b088..b97c763f 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -7,7 +7,8 @@ [reitit.impl :as impl] [reitit.ring :as ring] [reitit.core :as r]) - (:import (reitit Trie Trie$Matcher))) + (:import (reitit SegmentTrie Trie$Matcher) + (calfpath Util))) ;; ;; start repl with `lein perf repl` @@ -109,20 +110,20 @@ (impl/segments "/user/1234/profile/compact"))) (comment - (Trie/split "/user/1234/profile/compact") + (SegmentTrie/split "/user/1234/profile/compact") ;; 91ns (cc/quick-bench - (Trie/split "/user/1234/profile/compact"))) + (SegmentTrie/split "/user/1234/profile/compact"))) (comment (let [router (r/router ["/user/:id/profile/:type"])] (cc/quick-bench (r/match-by-path router "/user/1234/profile/compact")))) -(let [lookup ^Trie$Matcher (Trie/sample)] - (Trie/lookup lookup "/user/1234/profile/compact") +(let [lookup ^Trie$Matcher (SegmentTrie/sample)] + (SegmentTrie/lookup lookup "/user/1234/profile/compact") #_(cc/quick-bench - (Trie/lookup lookup "/user/1234/profile/compact"))) + (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) (let [router (r/router [["/user/:id" ::1] ["/user/:id/permissions" ::2] @@ -144,50 +145,50 @@ (read-string (str (.matcher - (doto (Trie.) + (doto (SegmentTrie.) (.add "/user" 1) #_(.add "/user/id/permissions" 2) (.add "/user/id/permissions2" 3))))) -(Trie/lookup +(SegmentTrie/lookup (.matcher - (doto (Trie.) + (doto (SegmentTrie.) (.add "/user/1" 1) (.add "/user/1/permissions" 2))) "/user/1") (.matcher - (doto (Trie.) + (doto (SegmentTrie.) (.add "/user/1" 1) (.add "/user/1/permissions" 2))) ;; 137ns (let [m (.matcher - (doto (Trie.) + (doto (SegmentTrie.) (.add "/user/:id/profile/:type" 1)))] #_(cc/quick-bench - (Trie/lookup m "/user/1234/profile/compact")) - (Trie/lookup m "/user/1234/profile/compact")) + (SegmentTrie/lookup m "/user/1234/profile/compact")) + (SegmentTrie/lookup m "/user/1234/profile/compact")) (comment - (let [matcher ^Trie$Matcher (Trie/sample)] - (Trie/lookup matcher "/user/1234/profile/compact") + (let [matcher ^Trie$Matcher (SegmentTrie/sample)] + (SegmentTrie/lookup matcher "/user/1234/profile/compact") (cc/quick-bench - (Trie/lookup matcher "/user/1234/profile/compact"))) + (SegmentTrie/lookup matcher "/user/1234/profile/compact"))) ;; 173ns - (let [lookup ^Trie$Matcher (Trie/tree2)] - (Trie/lookup lookup "/user/1234/profile/compact") + (let [lookup ^Trie$Matcher (SegmentTrie/tree2)] + (SegmentTrie/lookup lookup "/user/1234/profile/compact") (cc/quick-bench - (Trie/lookup lookup "/user/1234/profile/compact"))) + (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) ;; 140ns - (let [lookup ^Trie$Matcher (Trie/tree1)] - (Trie/lookup lookup "/user/1234/profile/compact") + (let [lookup ^Trie$Matcher (SegmentTrie/tree1)] + (SegmentTrie/lookup lookup "/user/1234/profile/compact") (cc/quick-bench - (Trie/lookup lookup "/user/1234/profile/compact"))) + (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) ;; 849ns (clojure, original) ;; 599ns (java, initial) @@ -225,15 +226,13 @@ (dotimes [_ 1000] (handler-reitit request))))) -(import '[reitit Util]) - (comment (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]) (cc/quick-bench (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])) (cc/quick-bench - (Trie/split "/user/1234/profile/compact/")) + (SegmentTrie/split "/user/1234/profile/compact/")) (cc/quick-bench (.split "/user/1234/profile/compact/" "/" 666))) @@ -265,9 +264,9 @@ (segment/lookup segment "/user/1/permissions/")))) #_(cc/quick-bench - (Trie/split "/user/1/profile/compat")) + (SegmentTrie/split "/user/1/profile/compat")) -#_(Trie/split "/user/1/profile/compat") +#_(SegmentTrie/split "/user/1/profile/compat") #_(cc/quick-bench (Segment2/hashLookup h "abba")) diff --git a/perf-test/clj/reitit/impl_perf_test.clj b/perf-test/clj/reitit/impl_perf_test.clj index 89fd604d..faae130a 100644 --- a/perf-test/clj/reitit/impl_perf_test.clj +++ b/perf-test/clj/reitit/impl_perf_test.clj @@ -190,7 +190,7 @@ (suite "split") ;; 114ns (String/split) - ;; 82ns (Trie/split) + ;; 82ns (SegmentTrie/split) (test "Splitting a String") (test! impl/segments "/olipa/kerran/:avaruus")) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index c507e9dc..67f8f695 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -3,7 +3,7 @@ [io.pedestal.http.route.prefix-tree :as p] [reitit.segment :as segment] [criterium.core :as cc]) - (:import (reitit Trie))) + (:import (reitit SegmentTrie))) ;; ;; testing @@ -72,7 +72,7 @@ (def matcher (.matcher - ^Trie + ^SegmentTrie (reduce (fn [acc [p d]] (segment/insert acc p d)) From 5079daa8f3b8195636bd18399a66d8ea507ce734 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 21:25:25 +0200 Subject: [PATCH 11/30] WIP --- .../java-src/reitit/SegmentTrie.java | 37 ++++++++++--------- modules/reitit-core/src/reitit/core.cljc | 4 +- modules/reitit-core/src/reitit/segment.cljc | 6 +-- modules/reitit-http/src/reitit/http.cljc | 4 +- modules/reitit-ring/src/reitit/ring.cljc | 4 +- perf-test/clj/reitit/calf_perf_test.clj | 10 ++--- 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 3cb58981..fd6c6b30 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -3,7 +3,7 @@ package reitit; import clojure.lang.Keyword; import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; +import java.net.URLDecoder; import java.util.*; public class SegmentTrie { @@ -30,7 +30,7 @@ public class SegmentTrie { if (s.contains("+")) { _s = s.replace("+", "%2B"); } - return URLEncoder.encode(_s, "UTF-8"); + return URLDecoder.decode(_s, "UTF-8"); } } catch (UnsupportedEncodingException ignored) { } @@ -72,6 +72,7 @@ public class SegmentTrie { SegmentTrie s = pointer.catchAll.get(k); if (s == null) { s = new SegmentTrie(); + s.data = data; pointer.catchAll.put(k, s); } break; @@ -103,7 +104,7 @@ public class SegmentTrie { public Matcher matcher() { Matcher m; if (!catchAll.isEmpty()) { - m = new CatchAllMatcher(catchAll.keySet().iterator().next(), catchAll.values().iterator().next().data); + m = new CatchAllMatcher(catchAll.keySet().iterator().next(), data); } else if (!wilds.isEmpty()) { List matchers = new ArrayList<>(); if (data != null) { @@ -118,12 +119,12 @@ public class SegmentTrie { m = new LinearMatcher(matchers); } else if (!childs.isEmpty()) { m = staticMatcher(); + if (data != null) { + m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m)); + } } else { return new DataMatcher(data); } - if (data != null) { - m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m)); - } return m; } @@ -165,10 +166,12 @@ public class SegmentTrie { @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; + if (i < segments.size() && !segments.get(i).isEmpty()) { + final Match m = child.match(i + 1, segments, match); + if (m != null) { + m.params.put(parameter, encode(segments.get(i))); + return m; + } } return null; } @@ -294,7 +297,7 @@ public class SegmentTrie { 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("public", new CatchAllMatcher(Keyword.intern("*"), new DataMatcher(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); } @@ -302,14 +305,14 @@ public class SegmentTrie { public static void main(String[] args) { SegmentTrie trie = new SegmentTrie(); - //trie.add("/kikka/:id/permissions", 1); - trie.add("/kikka/:id", 2); - trie.add("/kakka/ping", 3); + trie.add("/:abba", 1); + trie.add("/abba/1", 2); + trie.add("/:abba/:dabba/doo", 3); + trie.add("/abba/:dabba/boo", 4); Matcher m = trie.matcher(); System.err.println(m); - System.out.println(lookup(m, "/kikka/1/permissions")); - System.out.println(lookup(m, "/kikka/1")); - + System.err.println(m.getClass()); + System.out.println(lookup(m, "/abba")); /* SegmentTrie trie = new SegmentTrie(); trie.add("/user/:id/profile/:type", 1); diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 899410fc..24852b8f 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -289,7 +289,9 @@ names) (match-by-path [_ path] (if-let [match (segment/lookup pl path)] - (assoc (:data match) :path path))) + (-> (:data match) + (assoc :path-params (:path-params match)) + (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] (match nil))) diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index b0ff46fa..f62ff2f4 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -2,7 +2,7 @@ (:refer-clojure :exclude [-lookup compile]) (:require [reitit.impl :as impl] [clojure.string :as str]) - #?(:clj (:import (reitit SegmentTrie Trie$Match)))) + #?(:clj (:import (reitit SegmentTrie SegmentTrie$Match)))) (defrecord Match [data path-params]) @@ -54,9 +54,9 @@ (defn compile [segment] #?(:cljs segment - :clj (.matcher ^SegmentTrie segment))) + :clj (.matcher ^SegmentTrie (or segment (SegmentTrie.))))) (defn lookup [segment path] #?(:cljs (-lookup segment (impl/segments path) {}) - :clj (if-let [match ^Trie$Match (SegmentTrie/lookup segment path)] + :clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup segment path)] (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) diff --git a/modules/reitit-http/src/reitit/http.cljc b/modules/reitit-http/src/reitit/http.cljc index 75eda84d..ecb28fca 100644 --- a/modules/reitit-http/src/reitit/http.cljc +++ b/modules/reitit-http/src/reitit/http.cljc @@ -144,7 +144,7 @@ request (enrich-request request path-params match router)] (or (interceptor/execute executor interceptors request) (interceptor/execute executor default-queue request))) - (interceptor/execute executor default-queue (enrich-default-request request)))) + (interceptor/execute executor default-queue (enrich-default-request request router)))) ([request respond raise] (let [default #(interceptor/execute executor default-queue % respond raise)] (if-let [match (r/match-by-path router (:uri request))] @@ -160,7 +160,7 @@ (if interceptors (interceptor/execute executor interceptors request respond' raise) (default request))) - (default (enrich-default-request request)))) + (default (enrich-default-request request router)))) nil)) {::r/router router})))) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index a0913c2f..38df488a 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -285,7 +285,7 @@ handler (-> result method :handler (or default-handler)) request (enrich-request request path-params match router)] (or (handler request) (default-handler request))) - (default-handler (enrich-default-request request)))) + (default-handler (enrich-default-request request router)))) ([request respond raise] (if-let [match (r/match-by-path router (:uri request))] (let [method (:request-method request) @@ -294,7 +294,7 @@ handler (-> result method :handler (or default-handler)) request (enrich-request request path-params match router)] ((routes handler default-handler) request respond raise)) - (default-handler (enrich-default-request request) respond raise)) + (default-handler (enrich-default-request request router) respond raise)) nil))) {::r/router router})))) diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index b97c763f..298b450c 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -7,7 +7,7 @@ [reitit.impl :as impl] [reitit.ring :as ring] [reitit.core :as r]) - (:import (reitit SegmentTrie Trie$Matcher) + (:import (reitit SegmentTrie SegmentTrie$Matcher) (calfpath Util))) ;; @@ -120,7 +120,7 @@ (cc/quick-bench (r/match-by-path router "/user/1234/profile/compact")))) -(let [lookup ^Trie$Matcher (SegmentTrie/sample)] +(let [lookup ^SegmentTrie$Matcher (SegmentTrie/sample)] (SegmentTrie/lookup lookup "/user/1234/profile/compact") #_(cc/quick-bench (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) @@ -172,20 +172,20 @@ (comment - (let [matcher ^Trie$Matcher (SegmentTrie/sample)] + (let [matcher ^SegmentTrie$Matcher (SegmentTrie/sample)] (SegmentTrie/lookup matcher "/user/1234/profile/compact") (cc/quick-bench (SegmentTrie/lookup matcher "/user/1234/profile/compact"))) ;; 173ns - (let [lookup ^Trie$Matcher (SegmentTrie/tree2)] + (let [lookup ^SegmentTrie$Matcher (SegmentTrie/tree2)] (SegmentTrie/lookup lookup "/user/1234/profile/compact") (cc/quick-bench (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) ;; 140ns - (let [lookup ^Trie$Matcher (SegmentTrie/tree1)] + (let [lookup ^SegmentTrie$Matcher (SegmentTrie/tree1)] (SegmentTrie/lookup lookup "/user/1234/profile/compact") (cc/quick-bench (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) From 213f16defaf643168cc7e3f81d89ecf1fb449742 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 21:38:53 +0200 Subject: [PATCH 12/30] FIx swagger uris --- modules/reitit-swagger/src/reitit/swagger.cljc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index 47caeff4..b1551fdb 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -68,7 +68,8 @@ (->> (impl/segments path) (map #(if (impl/wild-or-catch-all-param? %) (str "{" (subs % 1) "}") %)) - (str/join "/"))) + (str/join "/") + (str "/"))) (defn create-swagger-handler [] "Create a ring handler to emit swagger spec. Collects all routes from router which have From b847af4ae7707b28437ec2c20e1858ecbe189f6d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 21:41:22 +0200 Subject: [PATCH 13/30] Fix segment tests --- test/cljc/reitit/impl_test.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index d7286d60..2ac3cc4b 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -3,9 +3,9 @@ [reitit.impl :as impl])) (deftest segments-test - (is (= ["" "api" "ipa" "beer" "craft" "bisse"] + (is (= ["api" "ipa" "beer" "craft" "bisse"] (into [] (impl/segments "/api/ipa/beer/craft/bisse")))) - (is (= ["" "a" "" "b" "" "c" ""] + (is (= ["a" "" "b" "" "c" ""] (into [] (impl/segments "/a//b//c/"))))) (deftest strip-nils-test From 16499cceb19f5fdd05c755bc2d9305dd9812559e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:02:53 +0200 Subject: [PATCH 14/30] Single wild optimization --- .../java-src/reitit/SegmentTrie.java | 31 ++++++++++--------- perf-test/clj/reitit/go_perf_test.clj | 2 ++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index fd6c6b30..ffd42936 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -106,17 +106,21 @@ public class SegmentTrie { if (!catchAll.isEmpty()) { m = new CatchAllMatcher(catchAll.keySet().iterator().next(), data); } else if (!wilds.isEmpty()) { - List matchers = new ArrayList<>(); - if (data != null) { - matchers.add(new DataMatcher(data)); + if(wilds.size() == 1 && data == null && childs.isEmpty()) { + m = new WildMatcher(wilds.keySet().iterator().next(), wilds.values().iterator().next().matcher()); + } else { + 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); } - 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(); if (data != null) { @@ -305,14 +309,11 @@ public class SegmentTrie { public static void main(String[] args) { SegmentTrie trie = new SegmentTrie(); - trie.add("/:abba", 1); - trie.add("/abba/1", 2); - trie.add("/:abba/:dabba/doo", 3); - trie.add("/abba/:dabba/boo", 4); + trie.add("/repos/:owner/:repo/stargazers", 1); Matcher m = trie.matcher(); System.err.println(m); System.err.println(m.getClass()); - System.out.println(lookup(m, "/abba")); + System.out.println(lookup(m, "/repos/metosin/reitit/stargazers")); /* SegmentTrie trie = new SegmentTrie(); trie.add("/user/:id/profile/:type", 1); diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 1114f573..a2961bc1 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -327,6 +327,7 @@ ;; 830ns (faster decode params) ;; 560µs (java-segment-router) ;; 490ns (java-segment-router, no injects) + ;; 440ns (java-segment-router, no injects, single-wild-optimization) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -337,6 +338,7 @@ ;; 160µs (faster decode params) ;; 120µs (java-segment-router) ;; 100µs (java-segment-router, no injects) + ;; 90µs (java-segment-router, no injects, single-wild-optimization) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench From 3168747540f7e9104ed6acd5939f311c589bb1bf Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:10:01 +0200 Subject: [PATCH 15/30] Fix cljs --- modules/reitit-core/src/reitit/impl.cljc | 10 ++++++++-- modules/reitit-core/src/reitit/segment.cljc | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index e996def8..f8deed9d 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -20,9 +20,15 @@ coll coll)) -(defn segments [path] +(defn segments + "Splits the path into sequence of segments, using `/` char. Assumes that the + path starts with `/`, stripping the first empty segment. e.g. + + (segments \"/a/b/c\") ; => (\"a\" \"b\" \"c\") + (segments \"/a/) ; => (\"a\" \"\")" + [path] #?(:clj (SegmentTrie/split ^String path) - :cljs (.split path #"/" 666))) + :cljs (rest (.split path #"/" 666)))) ;; ;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index f62ff2f4..b8a6be5f 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -57,6 +57,7 @@ :clj (.matcher ^SegmentTrie (or segment (SegmentTrie.))))) (defn lookup [segment path] - #?(:cljs (-lookup segment (impl/segments path) {}) + #?(:cljs (if-let [match (-lookup segment (impl/segments path) {})] + (assoc match :path-params (impl/url-decode-coll (:path-params match)))) :clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup segment path)] (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) From 36f1df3867dff8c91e752e6f9178aff87383f2a7 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:17:14 +0200 Subject: [PATCH 16/30] Dead code --- .../java-src/reitit/SegmentTrie.java | 35 +-- perf-test/clj/reitit/calf_perf_test.clj | 253 ++---------------- 2 files changed, 26 insertions(+), 262 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index ffd42936..c5cadcac 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -23,7 +23,7 @@ public class SegmentTrie { return segments; } - static String encode(String s) { + private static String encode(String s) { try { if (s.contains("%")) { String _s = s; @@ -293,19 +293,6 @@ public class SegmentTrie { 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("*"), new DataMatcher(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) { SegmentTrie trie = new SegmentTrie(); @@ -314,25 +301,5 @@ public class SegmentTrie { System.err.println(m); System.err.println(m.getClass()); System.out.println(lookup(m, "/repos/metosin/reitit/stargazers")); - /* - SegmentTrie trie = new SegmentTrie(); - 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.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/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 298b450c..32f678be 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -3,12 +3,9 @@ [reitit.perf-utils :refer :all] [ring.util.codec] [reitit.impl] - [reitit.segment :as segment] - [reitit.impl :as impl] [reitit.ring :as ring] [reitit.core :as r]) - (:import (reitit SegmentTrie SegmentTrie$Matcher) - (calfpath Util))) + (:import (reitit SegmentTrie))) ;; ;; start repl with `lein perf repl` @@ -85,237 +82,37 @@ :handler (fn [_] (h3x))}] ["/this/is/a/static/route" {:put (fn [_] (h40)) :handler (fn [_] (h4x))}]]) - (fn [_] (hxx)))) - -#_(let [request {:request-method :put - :uri "/this/is/a/static/route"}] - (handler-reitit request) - (cc/quick-bench - (handler-reitit request))) - -(let [request {:request-method :get - :uri "/user/1234/profile/compact/"}] - ;; OLD: 1338ns - ;; NEW: 981ns - ;; JAVA: 805ns - ;; NO-INJECT: 704ns - #_(cc/quick-bench - (handler-reitit request)) - (handler-reitit request)) - -(comment - (impl/segments "/user/1234/profile/compact") - ;; 145ns - (cc/quick-bench - (impl/segments "/user/1234/profile/compact"))) - -(comment - (SegmentTrie/split "/user/1234/profile/compact") - ;; 91ns - (cc/quick-bench - (SegmentTrie/split "/user/1234/profile/compact"))) - -(comment - (let [router (r/router ["/user/:id/profile/:type"])] - (cc/quick-bench - (r/match-by-path router "/user/1234/profile/compact")))) - -(let [lookup ^SegmentTrie$Matcher (SegmentTrie/sample)] - (SegmentTrie/lookup lookup "/user/1234/profile/compact") - #_(cc/quick-bench - (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) - -(let [router (r/router [["/user/:id" ::1] - ["/user/:id/permissions" ::2] - ["/company/:cid/dept/:did" ::3] - ["/this/is/a/static/route" ::4]])] - #_(cc/quick-bench - (r/match-by-path router "/user/1234/profile/compact")) - (r/match-by-path router "/user/1234")) - -;; 281ns -(let [router (r/router [["/user/:id/profile/:type" ::1] - ["/user/:id/permissions" ::2] - ["/company/:cid/dept/:did" ::3] - ["/this/is/a/static/route" ::4]])] - #_(cc/quick-bench - (r/match-by-path router "/user/1234/profile/compact")) - (r/match-by-path router "/user/1234/profile/compact")) - -(read-string - (str - (.matcher - (doto (SegmentTrie.) - (.add "/user" 1) - #_(.add "/user/id/permissions" 2) - (.add "/user/id/permissions2" 3))))) - -(SegmentTrie/lookup - (.matcher - (doto (SegmentTrie.) - (.add "/user/1" 1) - (.add "/user/1/permissions" 2))) - "/user/1") - -(.matcher - (doto (SegmentTrie.) - (.add "/user/1" 1) - (.add "/user/1/permissions" 2))) - -;; 137ns -(let [m (.matcher - (doto (SegmentTrie.) - (.add "/user/:id/profile/:type" 1)))] - #_(cc/quick-bench - (SegmentTrie/lookup m "/user/1234/profile/compact")) - (SegmentTrie/lookup m "/user/1234/profile/compact")) - -(comment - - (let [matcher ^SegmentTrie$Matcher (SegmentTrie/sample)] - (SegmentTrie/lookup matcher "/user/1234/profile/compact") - (cc/quick-bench - (SegmentTrie/lookup matcher "/user/1234/profile/compact"))) - - ;; 173ns - (let [lookup ^SegmentTrie$Matcher (SegmentTrie/tree2)] - (SegmentTrie/lookup lookup "/user/1234/profile/compact") - (cc/quick-bench - (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) - - - ;; 140ns - (let [lookup ^SegmentTrie$Matcher (SegmentTrie/tree1)] - (SegmentTrie/lookup lookup "/user/1234/profile/compact") - (cc/quick-bench - (SegmentTrie/lookup lookup "/user/1234/profile/compact"))) - - ;; 849ns (clojure, original) - ;; 599ns (java, initial) - ;; 173ns (fast split) - (let [router (r/router ["/user/:id/profile/:type"])] - (r/match-by-path router "/user/1234/profile/compact") - (cc/quick-bench - (r/match-by-path router "/user/1234/profile/compact"))) - - ;; 849ns (clojure, original) - ;; 599ns (java, initial) - ;; 173ns (java, optimized) - (let [router (r/router [["/user/:id/profile/:type/" ::1] - ["/user/:id/permissions/" ::2] - ["/company/:cid/dept/:did/" ::3] - ["/this/is/a/static/route" ::4]])] - (cc/quick-bench - (r/match-by-path router "/user/1234/profile/compact"))) - - ;; 131ns - (let [route ["/user/" :id "/profile/" :type "/"]] - (cc/quick-bench - (Util/matchURI "/user/1234/profile/compact/" route))) - - ;; 728ns - (cc/quick-bench - (r/match-by-path ring/ROUTER (:uri ring/REQUEST)))) - -(set! *warn-on-reflection* true) + (fn [_] (hxx)) + {:inject-match? false, :inject-router? false})) (comment (let [request {:request-method :get :uri "/user/1234/profile/compact/"}] - (time - (dotimes [_ 1000] - (handler-reitit request))))) - -(comment - (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]) - (cc/quick-bench - (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])) - - (cc/quick-bench - (SegmentTrie/split "/user/1234/profile/compact/")) - - (cc/quick-bench - (.split "/user/1234/profile/compact/" "/" 666))) - -(import '[reitit Segment2]) - -(def paths ["kikka" "kukka" "kakka" "abba" "jabba" "1" "2" "3" "4"]) -(def a (Segment2/createArray paths)) -(def h (Segment2/createHash paths)) - -(set! *warn-on-reflection* true) - -(comment - (let [segment (segment/create - [["/user/:id/profile/:type/" 1] - ["/user/:id/permissions/" 2] - ["/company/:cid/dept/:did/" 3] - ["/this/is/a/static/route" 4]])] - (segment/lookup segment "/user/1/profile/compat/") - - ;; OLD: 602ns - ;; NEW: 472ns - (cc/quick-bench - (segment/lookup segment "/user/1/profile/compat/")) - - ;; OLD: 454ns - ;; NEW: 372ns - (cc/quick-bench - (segment/lookup segment "/user/1/permissions/")))) - -#_(cc/quick-bench - (SegmentTrie/split "/user/1/profile/compat")) - -#_(SegmentTrie/split "/user/1/profile/compat") - -#_(cc/quick-bench - (Segment2/hashLookup h "abba")) - + ;; OLD: 1338ns + ;; NEW: 981ns + ;; JAVA: 805ns + ;; NO-INJECT: 704ns + #_(cc/quick-bench + (handler-reitit request)) + (handler-reitit request))) (comment - (cc/quick-bench - (dotimes [_ 1000] - ;; 7ns - (Segment2/arrayLookup a "abba"))) - - (cc/quick-bench - (dotimes [_ 1000] - ;; 3ns - (Segment2/hashLookup h "abba")))) + ;; 281ns + (let [router (r/router [["/user/:id/profile/:type" ::1] + ["/user/:id/permissions" ::2] + ["/company/:cid/dept/:did" ::3] + ["/this/is/a/static/route" ::4]])] + #_(cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact")) + (r/match-by-path router "/user/1234/profile/compact"))) (comment - (time - (dotimes [_ 1000] - (Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"]))) + (read-string + (str + (.matcher + (doto (SegmentTrie.) + (.add "/user" 1) + #_(.add "/user/id/permissions" 2) + (.add "/user/id/permissions2" 3)))))) - - (time - (let [s (s/create [["/user/:id/profile/:type/" 1]])] - (dotimes [_ 1000] - (s/lookup s "/user/1234/profile/compact/")))) - - (let [m {"/abba" 1}] - (time - (dotimes [_ 1000] - (get m "/abba")))) - - (time - (dotimes [_ 1000] - (Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false))) - - ;; 124ns - (cc/quick-bench - (Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false)) - - ;; 166ns - (cc/quick-bench - (impl/segments "/user/1234/profile/compact/")) - - ;; 597ns - (let [s (s/create [["/user/:id/profile/:type/" 1]])] - (cc/quick-bench - (s/lookup s "/user/1234/profile/compact/"))) - - (let [s (s/create [["/user/:id/profile/:type/" 1]])] - (s/lookup s "/user/1234/profile/compact/"))) From 406386e0f69179832b94b91f74020e2857ba4d62 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:25:28 +0200 Subject: [PATCH 17/30] encode -> decode --- modules/reitit-core/java-src/reitit/SegmentTrie.java | 8 ++++---- perf-test/clj/reitit/calf_perf_test.clj | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index c5cadcac..704efcce 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -23,7 +23,7 @@ public class SegmentTrie { return segments; } - private static String encode(String s) { + private static String decode(String s) { try { if (s.contains("%")) { String _s = s; @@ -173,7 +173,7 @@ public class SegmentTrie { if (i < segments.size() && !segments.get(i).isEmpty()) { final Match m = child.match(i + 1, segments, match); if (m != null) { - m.params.put(parameter, encode(segments.get(i))); + m.params.put(parameter, decode(segments.get(i))); return m; } } @@ -197,7 +197,7 @@ public class SegmentTrie { @Override public Match match(int i, List segments, Match match) { - match.params.put(parameter, encode(String.join("/", segments.subList(i, segments.size())))); + match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size())))); match.data = data; return match; } @@ -285,7 +285,7 @@ public class SegmentTrie { @Override public String toString() { - return (data != null ? data.toString() : "null"); + return (data != null ? data.toString() : "nil"); } } diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 32f678be..002793e9 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -113,6 +113,6 @@ (.matcher (doto (SegmentTrie.) (.add "/user" 1) - #_(.add "/user/id/permissions" 2) - (.add "/user/id/permissions2" 3)))))) + (.add "/user/id" 2) + (.add "/user/id/permissions2" nil)))))) From 3c2c1e47b24396256a996ad591c41f9fd9fa9da0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:29:03 +0200 Subject: [PATCH 18/30] . --- perf-test/clj/reitit/calf_perf_test.clj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 002793e9..6609f0b1 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -3,6 +3,7 @@ [reitit.perf-utils :refer :all] [ring.util.codec] [reitit.impl] + [clojure.edn :as edn] [reitit.ring :as ring] [reitit.core :as r]) (:import (reitit SegmentTrie))) @@ -108,11 +109,12 @@ (r/match-by-path router "/user/1234/profile/compact"))) (comment - (read-string + (edn/read-string (str (.matcher (doto (SegmentTrie.) (.add "/user" 1) - (.add "/user/id" 2) - (.add "/user/id/permissions2" nil)))))) + (.add "/user/:id" 2) + (.add "/user/:id/orders" 3) + (.add "/user/id/permissions" 4)))))) From da2975e66e42f86164c0cd225238fe834b8922f9 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:34:03 +0200 Subject: [PATCH 19/30] . --- perf-test/clj/reitit/prefix_tree_perf_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index 67f8f695..aa266152 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -108,7 +108,7 @@ ;; 1.0µs (Match records) ;; 0.63µs (Single sweep path paraµs) ;; 0.51µs (Cleanup) - ;; 0.33µs (Java) + ;; 0.30µs (Java) (cc/quick-bench (segment/lookup matcher "/v1/orgs/1/topics"))) From f850b923a5333cd625690b046dc099f8ac1f3852 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 13 Jan 2019 22:59:09 +0200 Subject: [PATCH 20/30] fix calfpath result --- perf-test/clj/reitit/opensensors_perf_test.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 7e39bdc1..eb6bc094 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -513,12 +513,12 @@ ;; 385ns (java-segment-router, no injects) (b! "reitit-ring-fast" reitit-ring-fast-f) - ;; 474ns (macros) - (b! "calfpath" calfpath-f) - ;; 2821ns (b! "pedestal" pedestal-f) + ;; 4364ns (macros) + (b! "calfpath" calfpath-f) + ;; 11615ns (b! "compojure" compojure-f) From 6bd005acbb4bc74744f62b461642872ec46d3809 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 12:16:46 +0200 Subject: [PATCH 21/30] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f41e525..e6c8ffe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### `reitit-core` * `reitit.core/Expand` can be extended, fixes [#201](https://github.com/metosin/reitit/issues/201). +* new snappy Java-backed `SegmentTrie` routing algorithm, wildcard routing is ~2x faster on the JVM ## 0.2.10 (2018-12-30) From 75065b56e394914ebc8ff063d6f08c27c277217a Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 16:38:45 +0200 Subject: [PATCH 22/30] Public docs for reitit.segment --- modules/reitit-core/src/reitit/segment.cljc | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index b8a6be5f..d51022df 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -48,16 +48,20 @@ ;; public api ;; -(defn insert [root path data] - #?(:cljs (-insert (or root (segment)) (impl/segments path) (map->Match {:data data})) - :clj (.add (or ^SegmentTrie root ^SegmentTrie (SegmentTrie.)) ^String path data))) +(defn insert + "Returns a Segment Trie with path with data inserted into it. Creates the trie if `nil`." + [trie path data] + #?(:cljs (-insert (or trie (segment)) (impl/segments path) (map->Match {:data data})) + :clj (.add (or ^SegmentTrie trie ^SegmentTrie (SegmentTrie.)) ^String path data))) -(defn compile [segment] - #?(:cljs segment - :clj (.matcher ^SegmentTrie (or segment (SegmentTrie.))))) +(defn compile [trie] + "Compiles the Trie so that [[lookup]] can be used." + #?(:cljs trie + :clj (.matcher ^SegmentTrie (or trie (SegmentTrie.))))) -(defn lookup [segment path] - #?(:cljs (if-let [match (-lookup segment (impl/segments path) {})] +(defn lookup [trie path] + "Looks the path from a Segment Trie. Returns a [[Match]] or `nil`." + #?(:cljs (if-let [match (-lookup trie (impl/segments path) {})] (assoc match :path-params (impl/url-decode-coll (:path-params match)))) - :clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup segment path)] + :clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup trie path)] (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) From ffc6ba805323ebdd7615300d9908180c74924152 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 16:39:00 +0200 Subject: [PATCH 23/30] Simplied Java, based on review --- .../reitit-core/java-src/reitit/SegmentTrie.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 704efcce..5813f26d 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -52,7 +52,7 @@ public class SegmentTrie { private Map childs = new HashMap<>(); private Map wilds = new HashMap<>(); - private Map catchAll = new HashMap<>(); + private Keyword catchAll = null; private Object data; public SegmentTrie add(String path, Object data) { @@ -69,12 +69,7 @@ public class SegmentTrie { pointer = s; } else if (p.startsWith("*")) { Keyword k = Keyword.intern(p.substring(1)); - SegmentTrie s = pointer.catchAll.get(k); - if (s == null) { - s = new SegmentTrie(); - s.data = data; - pointer.catchAll.put(k, s); - } + pointer.catchAll = k; break; } else { SegmentTrie s = pointer.childs.get(p); @@ -103,10 +98,10 @@ public class SegmentTrie { public Matcher matcher() { Matcher m; - if (!catchAll.isEmpty()) { - m = new CatchAllMatcher(catchAll.keySet().iterator().next(), data); + if (catchAll != null) { + m = new CatchAllMatcher(catchAll, data); } else if (!wilds.isEmpty()) { - if(wilds.size() == 1 && data == null && childs.isEmpty()) { + if (wilds.size() == 1 && data == null && childs.isEmpty()) { m = new WildMatcher(wilds.keySet().iterator().next(), wilds.values().iterator().next().matcher()); } else { List matchers = new ArrayList<>(); From 4c722d628ee48a1dbd3c0ca89041ea541d2797ab Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 17:27:16 +0200 Subject: [PATCH 24/30] Calfpath-data --- doc/images/opensensors.png | Bin 29958 -> 37115 bytes doc/performance.md | 13 +-- .../clj/reitit/opensensors_perf_test.clj | 75 +++++++++++++++++- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/doc/images/opensensors.png b/doc/images/opensensors.png index d59d6dab965bb85c9b746ac394deeafd36b481de..94bf228a70ee06c67836a722884217eefdaa3cbb 100644 GIT binary patch literal 37115 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 literal 29958 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
V?Mx8zW~%5Mu{mf0;vIf~-E|@iljFaPYz36F$R6 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 z zCbU1`YAnQu$XV^<=o!(*GSjN@rEQJ3XKB(y)9Oo9dtaaPp4l4WuCSCbO=tIXHWd3f zL4BkBafKaq{8msrFV42u%3P!X{QkYR6Zdr7F%iUkn@a z8bY6@9gL5xC_nL~>hN^_k_#*rogPfwyjF8`oH8QfpOBf*;*#N#54)Q`wHeyGW{P-9 zhHW25bv9w zr1zRv6nHACq^f07e6;+M!HcT=Vp?&7naLxo{V5zSI%>}oxao(>PaibBF+?iL6o{J! zdFhCspcBEO8N>^N56=$^7u6QV7q?M!cSJ_^&c_z>8@|EwO?C#|80RAqbhs7>u||kq z--iWBRC;r0>y$w9`^{DU!M4a2a;2zBt4NkLwE_O2lienActwl=BY(+XrA$Tb3pji*Qb7FUO$gtQx!p)jz!kQqjI z=KX%9EX&8nFO+Y(jvRk<1OfZ}jz=|Uvgka-ZO2t1)!_Jn^R!>WW2AD!NzA&1ajQMt z*8&Qgkc$(pdT+c@;{_4eVe+TXkJ48gNKs8uVDjZzbkJ?D&jRjwzuXy+RIdVfvk)md zj%2wZ55CB2KNEzbsoX3uyS6~Zu_ePbT=i9T=$@S_^>fGTIbR09ep7^hp#Zl+WO0i` zzjtS!Y_fIlTrWx^PWmGx=yha-x}v$!o5NjpQ372@mtRbnbXUZ3u_=^ySDb&<$735y z*-2y3iDw@f3R~_UbhvDBtE159T;sZuTP^M9)Mm+M2oE=&h&WHshm~2iUB3?4dw>e^;@DciO(anEZcHYVOUEQJn0m+Z_21-h;{Ev3G z%O!QpEZ^V5)6CdX_gLB4Pkp!Be0lu=+Z)OV*U5pVD8w;x;y6Yt+f&&~)tY3Vrdi2# zvE-h%k;~Zz$Ns^A&$jGRZ1K9!dtXU^)Jp&1?V;PVeJUwU8v8SVoqkJh3e8wC+enF208zPJU}iV@6QfjnGKv=64)SkX_~%~vokY0OX@+( zt8WI;t{kZ5Z%MWAL?JBurQy{ac-nYH{cs6KbEv~|T^=JB4hh1(<*!B0{7?!Hc8B7Pv!WH)FWJQK0 zQ|^%XPTvbS>lCQ7X>ozERDYJi*wZ*HS7cydawI_3y$wGk@o7N3c$9kfuyBDgUwiml zgn77+w?IN!P*%F1+}!;mMu;1Aa{1fUTw%$c!W%kKMtnX2n#OAI4;lcP_L=&x9= z)AkA6wQD*_tsx=?Ij#8O29t*xBlre&#yzzQnH5ZPOg;U~_q#%$k=u+t5sPs_h2 z!cLAUh5h3mwk>u?C9IO?h~J7c2Q=^lr~w>D+}jmt`+;b02mdle0+ZM5>*87(!!f2a zwEi}GfeF3tY`^mf0cyCBsE{hTplA8h3~U#pS3eeeTVYgayw#4G!ue%L-= z+?a~S_-%{8le2Fn{?K59;64vN!0#I!(mHULS>5UrguCT;m(iMV pnV*$X{r779|L^=y`Fz 1.9M ops/sec +;; Execution time mean (per 1000): 315 µs -> 3.2M ops/sec (cc/quick-bench (dotimes [_ 1000] (r/match-by-path routes "/workspace/1/1"))) ``` -Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 4-24x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal). +Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 6-40x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal). But, the example is too simple for any real benchmark. Also, some of the libraries always match on the `:request-method` too and by doing so, do more work than just match by path. Compojure does most work also by invoking the handler. @@ -79,7 +79,7 @@ So, we need to test something more realistic. To get better view on the real life routing performance, there is [test](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj) of a mid-size rest(ish) http api with 50+ routes, having a lot of path parameters. The route definitions are pulled off from the [OpenSensors](https://opensensors.io/) swagger definitions. -Thanks to the snappy new [segment-tree](https://github.com/metosin/reitit/blob/master/modules/reitit-core/src/reitit/segment.cljc) algorithm, `reitit-ring` is fastest here. Pedestal is also fast with it's [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) implementation. +Thanks to the snappy [SegmentTrie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/SegmentTrie.java) (a modification of [Radix tree](https://en.wikipedia.org/wiki/Radix_tree)), `reitit-ring` is fastest here. [Calfpath](https://github.com/kumarshantanu/calfpath) and [Pedestal](https://github.com/pedestal/pedestal) are also quite fast. ![Opensensors perf](images/opensensors.png) @@ -99,13 +99,14 @@ The reitit routing perf is measured to get an internal baseline to optimize agai ### Looking out of the box -A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is really awesome (if true). +A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is kinda awesome. ### Performance tips Few things that have an effect on performance: * Wildcard-routes are an order of magnitude slower than static routes -* It's ok to mix non-wildcard and wildcard routes in a same routing tree as long as you don't disable the [conflict resolution](basics/route_conflicts.md) => if no conflicting routes are found, a `:mixed-router` can be created, which internally has a fast static path router and a separate wildcard-router. So, the static paths are still fast. +* Conflicting routes are served with LinearRouter, which is the slowest implementation. +* It's ok to mix non-wildcard, wildcard or even conflicting routes in a same routing tree. Reitit will create an hierarchy of routers to serve all the routes with best possible implementation. * Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md) & [route data](advanced/configuring_routers.md). * Unmounted middleware (or interceptor) is infinitely faster than a mounted one effectively doing nothing. diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index eb6bc094..ba8c3813 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -9,6 +9,7 @@ [ataraxy.core :as ataraxy] [compojure.core :refer [routes context ANY]] [calfpath.core :as cp] + [calfpath.route :as cr] [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] @@ -367,7 +368,7 @@ ["/v1/users/:user-id/bookmarks" :get handler :route-name :test/route56] ["/v1/orgs/:org-id/topics" :get handler :route-name :test/route57]]))) -(defn opensensors-calfpath-handler [request] +(defn opensensors-calfpath-macro-handler [request] (cp/->uri request "/v2/whoami" [] (cp/->get request (handler request) nil) @@ -429,6 +430,68 @@ "/v1/orgs/:org-id/topics" [] (cp/->get request (handler request) nil) nil)) +(def opensensors-calfpath-routes + [{:uri "/v2/whoami" :nested [{:method :get, :handler handler}]} + {:uri "/v2/users/:user-id/datasets" :nested [{:method :get, :handler handler}]} + {:uri "/v2/public/projects/:project-id/datasets" :nested [{:method :get, :handler handler}]} + {:uri "/v1/public/topics/:topic" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/orgs/:org-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/search/topics/:term" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/invitations" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/devices/:batch/:type" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/topics" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/bookmarks/followers" :nested [{:method :get, :handler handler}]} + {:uri "/v2/datasets/:dataset-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/usage-stats" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/devices/:client-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/messages/user/:user-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/devices" :nested [{:method :get, :handler handler}]} + {:uri "/v1/public/users/:user-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/errors" :nested [{:method :get, :handler handler}]} + {:uri "/v1/public/orgs/:org-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/invitations" :nested [{:method :get, :handler handler}]} + {:uri "/v2/public/messages/dataset/bulk" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/devices/bulk" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/device-errors" :nested [{:method :get, :handler handler}]} + {:uri "/v2/login" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/usage-stats" :nested [{:method :get, :handler handler}]} + {:uri "/v2/users/:user-id/devices" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/claim-device/:client-id" :nested [{:method :get, :handler handler}]} + {:uri "/v2/public/projects/:project-id" :nested [{:method :get, :handler handler}]} + {:uri "/v2/public/datasets/:dataset-id" :nested [{:method :get, :handler handler}]} + {:uri "/v2/users/:user-id/topics/bulk" :nested [{:method :get, :handler handler}]} + {:uri "/v1/messages/device/:client-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/owned-orgs" :nested [{:method :get, :handler handler}]} + {:uri "/v1/topics/:topic" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/bookmark/:topic" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/members/:user-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/devices/:client-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/devices" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/members" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/members/invitation-data/:user-id" :nested [{:method :get, :handler handler}]} + {:uri "/v2/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]} + {:uri "/v1/whoami" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/api-key" :nested [{:method :get, :handler handler}]} + {:uri "/v2/schemas" :nested [{:method :get, :handler handler}]} + {:uri "/v2/users/:user-id/topics" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/confirm-membership/:token" :nested [{:method :get, :handler handler}]} + {:uri "/v2/topics/:topic" :nested [{:method :get, :handler handler}]} + {:uri "/v1/messages/topic/:topic" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/devices/:client-id/reset-password" :nested [{:method :get, :handler handler}]} + {:uri "/v2/topics" :nested [{:method :get, :handler handler}]} + {:uri "/v1/login" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/orgs" :nested [{:method :get, :handler handler}]} + {:uri "/v2/public/messages/dataset/:dataset-id" :nested [{:method :get, :handler handler}]} + {:uri "/v1/topics" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs" :nested [{:method :get, :handler handler}]} + {:uri "/v1/users/:user-id/bookmarks" :nested [{:method :get, :handler handler}]} + {:uri "/v1/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}]) + +(def opensensors-calfpath-data-handler + (partial cr/dispatch (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false}))) + (comment (pedestal/find-route (map-tree/router @@ -487,7 +550,8 @@ reitit-ring-f (ring/ring-handler (ring/router opensensors-routes)) reitit-ring-fast-f (ring/ring-handler (ring/router opensensors-routes) nil {:inject-router? false, :inject-match? false}) bidi-f #(bidi/match-route opensensors-bidi-routes (:uri %)) - calfpath-f opensensors-calfpath-handler + calfpath-macros-f opensensors-calfpath-macro-handler + calfpath-data-f opensensors-calfpath-data-handler ataraxy-f (partial ataraxy/matches opensensors-ataraxy-routes) compojure-f opensensors-compojure-routes pedestal-f (partial pedestal/find-route opensensors-pedestal-routes) @@ -513,11 +577,14 @@ ;; 385ns (java-segment-router, no injects) (b! "reitit-ring-fast" reitit-ring-fast-f) + ;; 2258ns + (b! "calfpath-data" calfpath-data-f) + ;; 2821ns (b! "pedestal" pedestal-f) - ;; 4364ns (macros) - (b! "calfpath" calfpath-f) + ;; 4364ns + (b! "calfpath-macros" calfpath-macros-f) ;; 11615ns (b! "compojure" compojure-f) From 9e58f93cc93fba2ed016555a63788f828d7d8ffe Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 20:41:58 +0200 Subject: [PATCH 25/30] test all calfpath perf --- perf-test/clj/reitit/opensensors_perf_test.clj | 17 ++++++++++++----- test/cljc/reitit/core_test.cljc | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index ba8c3813..b53c2b09 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -489,9 +489,12 @@ {:uri "/v1/users/:user-id/bookmarks" :nested [{:method :get, :handler handler}]} {:uri "/v1/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}]) -(def opensensors-calfpath-data-handler +(def opensensors-calfpath-walker-handler (partial cr/dispatch (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false}))) +(def opensensors-calfpath-unroll-handler + (cr/make-dispatcher (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false}))) + (comment (pedestal/find-route (map-tree/router @@ -551,7 +554,8 @@ reitit-ring-fast-f (ring/ring-handler (ring/router opensensors-routes) nil {:inject-router? false, :inject-match? false}) bidi-f #(bidi/match-route opensensors-bidi-routes (:uri %)) calfpath-macros-f opensensors-calfpath-macro-handler - calfpath-data-f opensensors-calfpath-data-handler + calfpath-walker-f opensensors-calfpath-walker-handler + calfpath-unroll-f opensensors-calfpath-unroll-handler ataraxy-f (partial ataraxy/matches opensensors-ataraxy-routes) compojure-f opensensors-compojure-routes pedestal-f (partial pedestal/find-route opensensors-pedestal-routes) @@ -577,13 +581,16 @@ ;; 385ns (java-segment-router, no injects) (b! "reitit-ring-fast" reitit-ring-fast-f) - ;; 2258ns - (b! "calfpath-data" calfpath-data-f) + ;; 2137ns + (b! "calfpath-walker" calfpath-walker-f) + + ;; 4774ns + (b! "calfpath-unroll" calfpath-unroll-f) ;; 2821ns (b! "pedestal" pedestal-f) - ;; 4364ns + ;; 4803ns (b! "calfpath-macros" calfpath-macros-f) ;; 11615ns diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index c64525e8..a5d5b1e9 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -17,6 +17,8 @@ (is (= [["/api/ipa/:size" {:name ::beer}]] (r/routes router))) (is (map? (r/options router))) + (is (= nil + (r/match-by-path router "/api"))) (is (= (r/map->Match {:template "/api/ipa/:size" :data {:name ::beer} @@ -108,6 +110,8 @@ (is (= [["/api/ipa/large" {:name ::beer}]] (r/routes router))) (is (map? (r/options router))) + (is (= nil + (r/match-by-path router "/api"))) (is (= (r/map->Match {:template "/api/ipa/large" :data {:name ::beer} From 393049a7724fc7ddcf9baefc2a1b7d63788ab777 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 21:08:20 +0200 Subject: [PATCH 26/30] Oh My Java: safe guard against index overflows. --- .../java-src/reitit/SegmentTrie.java | 17 +++++++++++------ test/cljc/reitit/core_test.cljc | 5 ++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 5813f26d..31de24ff 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -192,9 +192,12 @@ public class SegmentTrie { @Override public Match match(int i, List segments, Match match) { - match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size())))); - match.data = data; - return match; + if (i < segments.size()) { + match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size())))); + match.data = data; + return match; + } + return null; } @Override @@ -212,9 +215,11 @@ public class SegmentTrie { @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); + if (i < segments.size()) { + final Matcher child = map.get(segments.get(i)); + if (child != null) { + return child.match(i + 1, segments, match); + } } return null; } diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index a5d5b1e9..4905b6f7 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -74,8 +74,9 @@ ["/abba/1" ::abba2] ["/:jabba/2" ::jabba2] ["/:abba/:dabba/doo" ::doo] + ["/abba/dabba/boo/baa" ::baa] ["/abba/:dabba/boo" ::boo] - ["/:jabba/:dabba/:doo/*foo" ::wild]] + ["/:jabba/:dabba/:doo/:daa/*foo" ::wild]] {:router r}) matches #(-> router (r/match-by-path %) :data :name)] (is (= ::abba (matches "/abba"))) @@ -83,6 +84,8 @@ (is (= ::jabba2 (matches "/abba/2"))) (is (= ::doo (matches "/abba/1/doo"))) (is (= ::boo (matches "/abba/1/boo"))) + (is (= ::baa (matches "/abba/dabba/boo/baa"))) + (is (= ::boo (matches "/abba/dabba/boo"))) (is (= ::wild (matches "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))))) (testing "empty path segments" From e7bf1000b9474c2e93c397a63c169c3454c19595 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 Jan 2019 21:53:37 +0200 Subject: [PATCH 27/30] More perf docs --- CHANGELOG.md | 8 ++++++++ doc/performance.md | 28 ++++++++++++++++++++++++++++ doc/ring/ring.md | 8 +++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6c8ffe1..28bf4569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ * `reitit.core/Expand` can be extended, fixes [#201](https://github.com/metosin/reitit/issues/201). * new snappy Java-backed `SegmentTrie` routing algorithm, wildcard routing is ~2x faster on the JVM +### `reitit-ring` + +* new options `:inject-match?` and `:inject-router?` on `reitit.ring/ring-handler` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details. + +### `reitit-http` + +* new options `:inject-match?` and `:inject-router?` on `reitit.http/ring-handler` and `reitit.http/routing-interceptor` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details. + ## 0.2.10 (2018-12-30) ### `reitit-core` diff --git a/doc/performance.md b/doc/performance.md index 466f0b39..13d9491f 100644 --- a/doc/performance.md +++ b/doc/performance.md @@ -101,6 +101,34 @@ The reitit routing perf is measured to get an internal baseline to optimize agai A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is kinda awesome. +### Faster! + +By default, `reitit.ring/ring-router`, `reitit.http/ring-router` and `reitit.http/routing-interceptor` inject both `Match` and `Router` into the request. You can remove the injections setting options `:inject-match?` and `:inject-router?` to `false`. This saves some tens of nanos (with the hw described above). + +```clj +(require '[reitit.ring :as ring]) +(require '[criterium.core :as cc]) + +(defn create [options] + (ring/ring-handler + (ring/router + ["/ping" (constantly {:status 200, :body "ok"})]) + (ring/create-default-handler) + options)) + +;; 130ns +(let [app (create nil)] + (cc/quick-bench + (app {:request-method :get, :uri "/ping"}))) + +;; 80ns +(let [app (create {:inject-router? false, :inject-match? false})] + (cc/quick-bench + (app {:request-method :get, :uri "/ping"}))) +``` + +**NOTE**: Without `Router`, you can't to do [reverse routing](ring/reverse_routing.md) and without `Match` you can't write [dynamic extensions](ring/dynamic_extensions.md). + ### Performance tips Few things that have an effect on performance: diff --git a/doc/ring/ring.md b/doc/ring/ring.md index ff1f5deb..2356d0b5 100644 --- a/doc/ring/ring.md +++ b/doc/ring/ring.md @@ -51,9 +51,11 @@ Match contains `:result` compiled by the `ring-router`: Given a `ring-router`, optional default-handler & options, `ring-handler` function will return a valid ring handler supporting both synchronous and [asynchronous](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) request handling. The following options are available: -| key | description | -| --------------|-------------| -| `:middleware` | Optional sequence of middleware that wrap the ring-handler" +| key | description | +| ------------------|-------------| +| `:middleware` | Optional sequence of middleware that wrap the ring-handler" +| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true) +| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true) Simple Ring app: From 5bd933a10c7dddc1a3798d2ef78237fc4d4f393e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Jan 2019 22:04:50 +0200 Subject: [PATCH 28/30] docs --- CHANGELOG.md | 2 +- doc/performance.md | 8 ++++---- modules/reitit-core/java-src/reitit/SegmentTrie.java | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28bf4569..7378a277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### `reitit-core` * `reitit.core/Expand` can be extended, fixes [#201](https://github.com/metosin/reitit/issues/201). -* new snappy Java-backed `SegmentTrie` routing algorithm, wildcard routing is ~2x faster on the JVM +* new snappy Java-backed `SegmentTrie` routing algorithm and data structure, making wildcard routing ~2x faster on the JVM ### `reitit-ring` diff --git a/doc/performance.md b/doc/performance.md index 13d9491f..76ef31c9 100644 --- a/doc/performance.md +++ b/doc/performance.md @@ -1,6 +1,6 @@ # Performance -Besides having great features, the goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten. +Besides having great features, goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten. ![Opensensors perf test](images/opensensors.png) @@ -79,13 +79,13 @@ So, we need to test something more realistic. To get better view on the real life routing performance, there is [test](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj) of a mid-size rest(ish) http api with 50+ routes, having a lot of path parameters. The route definitions are pulled off from the [OpenSensors](https://opensensors.io/) swagger definitions. -Thanks to the snappy [SegmentTrie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/SegmentTrie.java) (a modification of [Radix tree](https://en.wikipedia.org/wiki/Radix_tree)), `reitit-ring` is fastest here. [Calfpath](https://github.com/kumarshantanu/calfpath) and [Pedestal](https://github.com/pedestal/pedestal) are also quite fast. +Thanks to the snappy [SegmentTrie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/SegmentTrie.java) (a modification of [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree)), `reitit-ring` is fastest here. [Calfpath](https://github.com/kumarshantanu/calfpath) and [Pedestal](https://github.com/pedestal/pedestal) are also quite fast. ![Opensensors perf](images/opensensors.png) ### CQRS apis -Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html)-style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste). +Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html) style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste). Both `reitit-ring` and Pedestal shine in this test, thanks to the fast lookup-routers. On average, they are **two** and on best case, **three orders of magnitude faster** than the other tested libs. Ataraxy failed this test on `Method code too large!` error. @@ -136,5 +136,5 @@ Few things that have an effect on performance: * Wildcard-routes are an order of magnitude slower than static routes * Conflicting routes are served with LinearRouter, which is the slowest implementation. * It's ok to mix non-wildcard, wildcard or even conflicting routes in a same routing tree. Reitit will create an hierarchy of routers to serve all the routes with best possible implementation. -* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md) & [route data](advanced/configuring_routers.md). +* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md), [interceptors](http/interceptors.md) and [route data](advanced/configuring_routers.md). * Unmounted middleware (or interceptor) is infinitely faster than a mounted one effectively doing nothing. diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 31de24ff..77422d6e 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -68,8 +68,7 @@ public class SegmentTrie { } pointer = s; } else if (p.startsWith("*")) { - Keyword k = Keyword.intern(p.substring(1)); - pointer.catchAll = k; + pointer.catchAll = Keyword.intern(p.substring(1)); break; } else { SegmentTrie s = pointer.childs.get(p); From 433cf9102d36203587c8a386d3d9c8bd9bf45eee Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Jan 2019 22:05:00 +0200 Subject: [PATCH 29/30] . --- test/cljc/reitit/segment_test.cljc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 test/cljc/reitit/segment_test.cljc diff --git a/test/cljc/reitit/segment_test.cljc b/test/cljc/reitit/segment_test.cljc new file mode 100644 index 00000000..696e44ba --- /dev/null +++ b/test/cljc/reitit/segment_test.cljc @@ -0,0 +1,12 @@ +(ns reitit.segment-test + (:require [clojure.test :refer [deftest testing is are]] + [reitit.segment :as s])) + +(-> (s/insert nil "/foo" {:a 1}) (s/compile) (s/lookup "/foo")) +; => #reitit.segment.Match{:data {:a 1}, :path-params {}} + +(-> (s/insert nil "/foo" {:a 1}) (s/insert "/foo/*" {:b 1}) (s/compile) (s/lookup "/foo")) +; => nil + +(-> (s/insert nil "/foo" {:a 1}) (s/insert "/foo/*" {:b 1}) (s/compile) (s/lookup "/foo/bar")) +; => #reitit.segment.Match{:data {:b 1}, :path-params {: "bar"}} From b8d82862652356c8970193ca1aab01c51b122854 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Jan 2019 22:13:12 +0200 Subject: [PATCH 30/30] Fixes for the SegmentTrie --- .../java-src/reitit/SegmentTrie.java | 17 +++++++++++---- test/cljc/reitit/segment_test.cljc | 21 +++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 77422d6e..2337d893 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -52,7 +52,7 @@ public class SegmentTrie { private Map childs = new HashMap<>(); private Map wilds = new HashMap<>(); - private Keyword catchAll = null; + private Map catchAll = new HashMap<>(); private Object data; public SegmentTrie add(String path, Object data) { @@ -68,7 +68,13 @@ public class SegmentTrie { } pointer = s; } else if (p.startsWith("*")) { - pointer.catchAll = Keyword.intern(p.substring(1)); + Keyword k = Keyword.intern(p.substring(1)); + SegmentTrie s = pointer.catchAll.get(k); + if (s == null) { + s = new SegmentTrie(); + pointer.catchAll.put(k, s); + } + pointer = s; break; } else { SegmentTrie s = pointer.childs.get(p); @@ -97,8 +103,11 @@ public class SegmentTrie { public Matcher matcher() { Matcher m; - if (catchAll != null) { - m = new CatchAllMatcher(catchAll, data); + if (!catchAll.isEmpty()) { + m = new CatchAllMatcher(catchAll.keySet().iterator().next(), catchAll.values().iterator().next().data); + if (data != null) { + m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m)); + } } else if (!wilds.isEmpty()) { if (wilds.size() == 1 && data == null && childs.isEmpty()) { m = new WildMatcher(wilds.keySet().iterator().next(), wilds.values().iterator().next().matcher()); diff --git a/test/cljc/reitit/segment_test.cljc b/test/cljc/reitit/segment_test.cljc index 696e44ba..ca7bd2bf 100644 --- a/test/cljc/reitit/segment_test.cljc +++ b/test/cljc/reitit/segment_test.cljc @@ -2,11 +2,20 @@ (:require [clojure.test :refer [deftest testing is are]] [reitit.segment :as s])) -(-> (s/insert nil "/foo" {:a 1}) (s/compile) (s/lookup "/foo")) -; => #reitit.segment.Match{:data {:a 1}, :path-params {}} +(deftest tests + (is (= (s/->Match {:a 1} {}) + (-> (s/insert nil "/foo" {:a 1}) + (s/compile) + (s/lookup "/foo")))) -(-> (s/insert nil "/foo" {:a 1}) (s/insert "/foo/*" {:b 1}) (s/compile) (s/lookup "/foo")) -; => nil + (is (= (s/->Match {:a 1} {}) + (-> (s/insert nil "/foo" {:a 1}) + (s/insert "/foo/*bar" {:b 1}) + (s/compile) + (s/lookup "/foo")))) -(-> (s/insert nil "/foo" {:a 1}) (s/insert "/foo/*" {:b 1}) (s/compile) (s/lookup "/foo/bar")) -; => #reitit.segment.Match{:data {:b 1}, :path-params {: "bar"}} + (is (= (s/->Match {:b 1} {:bar "bar"}) + (-> (s/insert nil "/foo" {:a 1}) + (s/insert "/foo/*bar" {:b 1}) + (s/compile) + (s/lookup "/foo/bar")))))