create empty path-parameters at creation time, 266ns->251ns (-6%)

This commit is contained in:
Tommi Reiman 2019-02-09 19:47:41 +02:00
parent 950fef88d2
commit 9422cd28c1
8 changed files with 80 additions and 65 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -40,9 +40,14 @@ public class Trie {
} }
public static class Match { public static class Match {
public IPersistentMap params = PersistentArrayMap.EMPTY; public IPersistentMap params;
public Object data; public Object data;
public Match(IPersistentMap params, Object data) {
this.params = params;
this.data = data;
}
@Override @Override
public String toString() { public String toString() {
Map<Object, Object> m = new HashMap<>(); Map<Object, Object> m = new HashMap<>();
@ -53,7 +58,7 @@ public class Trie {
} }
public interface Matcher { public interface Matcher {
Match match(int i, int max, char[] path, Match match); Match match(int i, int max, char[] path);
int depth(); int depth();
@ -76,7 +81,7 @@ public class Trie {
} }
@Override @Override
public Match match(int i, int max, char[] path, Match match) { public Match match(int i, int max, char[] path) {
if (max < i + size) { if (max < i + size) {
return null; return null;
} }
@ -85,7 +90,7 @@ public class Trie {
return null; return null;
} }
} }
return child.match(i + size, max, path, match); return child.match(i + size, max, path);
} }
@Override @Override
@ -104,22 +109,23 @@ public class Trie {
} }
} }
public static DataMatcher dataMatcher(Object data) { public static DataMatcher dataMatcher(IPersistentMap params, Object data) {
return new DataMatcher(data); return new DataMatcher(params, data);
} }
static final class DataMatcher implements Matcher { static final class DataMatcher implements Matcher {
private final IPersistentMap params;
private final Object data; private final Object data;
DataMatcher(Object data) { DataMatcher(IPersistentMap params, Object data) {
this.params = params;
this.data = data; this.data = data;
} }
@Override @Override
public Match match(int i, int max, char[] path, Match match) { public Match match(int i, int max, char[] path) {
if (i == max) { if (i == max) {
match.data = data; return new Match(params, data);
return match;
} }
return null; return null;
} }
@ -156,7 +162,7 @@ public class Trie {
} }
@Override @Override
public Match match(int i, int max, char[] path, Match match) { public Match match(int i, int max, char[] path) {
if (i < max && path[i] != end) { if (i < max && path[i] != end) {
int stop = max; int stop = max;
for (int j = i; j < max; j++) { for (int j = i; j < max; j++) {
@ -166,7 +172,7 @@ public class Trie {
break; break;
} }
} }
final Match m = child.match(stop, max, path, match); final Match m = child.match(stop, max, path);
if (m != null) { if (m != null) {
m.params = m.params.assoc(key, decode(path, i, stop)); m.params = m.params.assoc(key, decode(path, i, stop));
} }
@ -191,25 +197,25 @@ public class Trie {
} }
} }
public static CatchAllMatcher catchAllMatcher(Keyword parameter, Object data) { public static CatchAllMatcher catchAllMatcher(Keyword parameter, IPersistentMap params, Object data) {
return new CatchAllMatcher(parameter, data); return new CatchAllMatcher(parameter, params, data);
} }
static final class CatchAllMatcher implements Matcher { static final class CatchAllMatcher implements Matcher {
private final Keyword parameter; private final Keyword parameter;
private final IPersistentMap params;
private final Object data; private final Object data;
CatchAllMatcher(Keyword parameter, Object data) { CatchAllMatcher(Keyword parameter, IPersistentMap params, Object data) {
this.parameter = parameter; this.parameter = parameter;
this.params = params;
this.data = data; this.data = data;
} }
@Override @Override
public Match match(int i, int max, char[] path, Match match) { public Match match(int i, int max, char[] path) {
if (i < max) { if (i < max) {
match.params = match.params.assoc(parameter, decode(path, i, max)); return new Match(params.assoc(parameter, decode(path, i, max)), data);
match.data = data;
return match;
} }
return null; return null;
} }
@ -226,7 +232,7 @@ public class Trie {
@Override @Override
public String toString() { public String toString() {
return "[" + parameter + " " + new DataMatcher(data) + "]"; return "[" + parameter + " " + new DataMatcher(null, data) + "]";
} }
} }
@ -246,9 +252,9 @@ public class Trie {
} }
@Override @Override
public Match match(int i, int max, char[] path, Match match) { public Match match(int i, int max, char[] path) {
for (int j = 0; j < size; j++) { for (int j = 0; j < size; j++) {
final Match m = childs[j].match(i, max, path, match); final Match m = childs[j].match(i, max, path);
if (m != null) { if (m != null) {
return m; return m;
} }
@ -273,7 +279,7 @@ public class Trie {
} }
public static Object lookup(Matcher matcher, String path) { public static Object lookup(Matcher matcher, String path) {
return matcher.match(0, path.length(), path.toCharArray(), new Match()); return matcher.match(0, path.length(), path.toCharArray());
} }
public static void main(String[] args) { public static void main(String[] args) {
@ -283,8 +289,8 @@ public class Trie {
staticMatcher("/auth/", staticMatcher("/auth/",
linearMatcher( linearMatcher(
Arrays.asList( Arrays.asList(
staticMatcher("login", dataMatcher(1)), staticMatcher("login", dataMatcher(null, 1)),
staticMatcher("recovery", dataMatcher(2))))))); staticMatcher("recovery", dataMatcher(null, 2)))))));
System.err.println(matcher); System.err.println(matcher);
System.out.println(lookup(matcher, "/auth/login")); System.out.println(lookup(matcher, "/auth/login"));
System.out.println(lookup(matcher, "/auth/recovery")); System.out.println(lookup(matcher, "/auth/recovery"));

View file

@ -131,7 +131,7 @@
(match-by-path [_ path] (match-by-path [_ path]
(if-let [match (trie/lookup matcher path)] (if-let [match (trie/lookup matcher path)]
(-> (:data match) (-> (:data match)
(assoc :path-params (:path-params match)) (assoc :path-params (:params match))
(assoc :path path)))) (assoc :path path))))
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]
@ -220,7 +220,7 @@
(match-by-path [_ path] (match-by-path [_ path]
(if-let [match (trie/lookup pl path)] (if-let [match (trie/lookup pl path)]
(-> (:data match) (-> (:data match)
(assoc :path-params (:path-params match)) (assoc :path-params (:params match))
(assoc :path path)))) (assoc :path path))))
(match-by-name [_ name] (match-by-name [_ name]
(if-let [match (impl/fast-get lookup name)] (if-let [match (impl/fast-get lookup name)]

View file

@ -7,8 +7,8 @@
(defrecord Wild [value]) (defrecord Wild [value])
(defrecord CatchAll [value]) (defrecord CatchAll [value])
(defrecord Match [data path-params]) (defrecord Match [params data])
(defrecord Node [children wilds catch-all data]) (defrecord Node [children wilds catch-all params data])
(defn wild? [x] (instance? Wild x)) (defn wild? [x] (instance? Wild x))
(defn catch-all? [x] (instance? CatchAll x)) (defn catch-all? [x] (instance? CatchAll x))
@ -19,9 +19,9 @@
(depth [this]) (depth [this])
(length [this])) (length [this]))
(defn assoc-path-param [match k v] (defn assoc-param [match k v]
(let [params (:path-params match)] (let [params (:params match)]
(assoc match :path-params (assoc params k v)))) (assoc match :params (assoc params k v))))
;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix
(defn common-prefix [s1 s2] (defn common-prefix [s1 s2]
@ -121,25 +121,25 @@
;; ;;
(defn- -node [m] (defn- -node [m]
(map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) (map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m)))
(defn- -insert [node [path & ps] data] (defn- -insert [node [path & ps] params data]
(let [node' (cond (let [node' (cond
(nil? path) (nil? path)
(assoc node :data data) (assoc node :data data :params params)
(instance? Wild path) (instance? Wild path)
(let [next (first ps)] (let [next (first ps)]
(if (or (instance? Wild next) (instance? CatchAll next)) (if (or (instance? Wild next) (instance? CatchAll next))
(ex/fail! (str "Two following wilds: " path ", " next)) (ex/fail! (str "Two following wilds: " path ", " next))
(update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))))) (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data)))))
(instance? CatchAll path) (instance? CatchAll path)
(assoc-in node [:catch-all path] (-node {:data data})) (assoc-in node [:catch-all path] (-node {:params params, :data data}))
(str/blank? path) (str/blank? path)
(-insert node ps data) (-insert node ps params data)
:else :else
(or (or
@ -148,20 +148,20 @@
(if-let [cp (common-prefix p path)] (if-let [cp (common-prefix p path)]
(if (= cp p) (if (= cp p)
;; insert into child node ;; insert into child node
(let [n' (-insert n (conj ps (subs path (count p))) data)] (let [n' (-insert n (conj ps (subs path (count p))) params data)]
(reduced (assoc-in node [:children p] n'))) (reduced (assoc-in node [:children p] n')))
;; split child node ;; split child node
(let [rp (subs p (count cp)) (let [rp (subs p (count cp))
rp' (subs path (count cp)) rp' (subs path (count cp))
n' (-insert (-node {}) ps data) n' (-insert (-node {}) ps params data)
n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil)] n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)]
(reduced (update node :children (fn [children] (reduced (update node :children (fn [children]
(-> children (-> children
(dissoc p) (dissoc p)
(assoc cp n''))))))))) (assoc cp n'')))))))))
nil (:children node)) nil (:children node))
;; new child node ;; new child node
(assoc-in node [:children path] (-insert (-node {}) ps data))))] (assoc-in node [:children path] (-insert (-node {}) ps params data))))]
(if-let [child (get-in node' [:children ""])] (if-let [child (get-in node' [:children ""])]
;; optimize by removing empty paths ;; optimize by removing empty paths
(-> (merge-with merge (dissoc node' :data) child) (-> (merge-with merge (dissoc node' :data) child)
@ -173,9 +173,9 @@
(let [param (subs path start end)] (let [param (subs path start end)]
(if percent? (js/decodeURIComponent param) param)))) (if percent? (js/decodeURIComponent param) param))))
(defn data-matcher [data] (defn data-matcher [params data]
#?(:clj (Trie/dataMatcher data) #?(:clj (Trie/dataMatcher params data)
:cljs (let [match (->Match data {})] :cljs (let [match (->Match params data)]
(reify Matcher (reify Matcher
(match [_ i max _] (match [_ i max _]
(if (= i max) (if (= i max)
@ -207,23 +207,23 @@
(loop [percent? false, j i] (loop [percent? false, j i]
(if (= max j) (if (= max j)
(if-let [match (match matcher max max path)] (if-let [match (match matcher max max path)]
(assoc-path-param match key (decode path i max percent?))) (assoc-param match key (decode path i max percent?)))
(let [c ^char (get path j)] (let [c ^char (get path j)]
(condp = c (condp = c
end (if-let [match (match matcher j max path)] end (if-let [match (match matcher j max path)]
(assoc-path-param match key (decode path i j percent?))) (assoc-param match key (decode path i j percent?)))
\% (recur true (inc j)) \% (recur true (inc j))
(recur percent? (inc j)))))))) (recur percent? (inc j))))))))
(view [_] [key (view matcher)]) (view [_] [key (view matcher)])
(depth [_] (inc (depth matcher))) (depth [_] (inc (depth matcher)))
(length [_])))) (length [_]))))
(defn catch-all-matcher [key data] (defn catch-all-matcher [key params data]
#?(:clj (Trie/catchAllMatcher key data) #?(:clj (Trie/catchAllMatcher key params data)
:cljs (let [match (->Match data nil)] :cljs (let [match (->Match params data)]
(reify Matcher (reify Matcher
(match [_ i max path] (match [_ i max path]
(if (< i max) (assoc-path-param match key (decode path i max true)))) (if (< i max) (assoc-param match key (decode path i max true))))
(view [_] [key [data]]) (view [_] [key [data]])
(depth [_] 1) (depth [_] 1)
(length [_]))))) (length [_])))))
@ -255,12 +255,14 @@
(insert acc p d)) (insert acc p d))
node routes)) node routes))
([node path data] ([node path data]
(-insert (or node (-node {})) (split-path path) data))) (let [parts (split-path path)
params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))]
(-insert (or node (-node {})) (split-path path) params data))))
(defn compile [{:keys [data children wilds catch-all]}] (defn compile [{:keys [data params children wilds catch-all] :or {params {}}}]
(let [ends (fn [{:keys [children]}] (or (keys children) ["/"])) (let [ends (fn [{:keys [children]}] (or (keys children) ["/"]))
matchers (-> [] matchers (-> []
(cond-> data (conj (data-matcher data))) (cond-> data (conj (data-matcher params data)))
(into (for [[p c] children] (static-matcher p (compile c)))) (into (for [[p c] children] (static-matcher p (compile c))))
(into (into
(for [[p c] wilds] (for [[p c] wilds]
@ -269,11 +271,11 @@
(if (next ends) (if (next ends)
(ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends)) (ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends))
(wild-matcher p (ffirst ends) (compile c)))))) (wild-matcher p (ffirst ends) (compile c))))))
(into (for [[p c] catch-all] (catch-all-matcher (:value p) (:data c)))))] (into (for [[p c] catch-all] (catch-all-matcher (:value p) params (:data c)))))]
(cond (cond
(> (count matchers) 1) (linear-matcher matchers) (> (count matchers) 1) (linear-matcher matchers)
(= (count matchers) 1) (first matchers) (= (count matchers) 1) (first matchers)
:else (data-matcher nil)))) :else (data-matcher {} nil))))
(defn pretty [matcher] (defn pretty [matcher]
#?(:clj (-> matcher str read-string eval) #?(:clj (-> matcher str read-string eval)
@ -281,9 +283,9 @@
(defn lookup [matcher path] (defn lookup [matcher path]
#?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)]
(->Match (.data match) (.params match))) (->Match (.params match) (.data match)))
:cljs (if-let [match (match matcher 0 (count path) path)] :cljs (if-let [match (match matcher 0 (count path) path)]
(->Match (:data match) (:path-params match))))) (->Match (:params match) (:data match)))))
;; ;;
;; spike ;; spike

View file

@ -333,6 +333,7 @@
;; 281ns (trie-router, no injects, optimized) ;; 281ns (trie-router, no injects, optimized)
;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure
;; 273ns (trie-router, no injects, direct-data) ;; 273ns (trie-router, no injects, direct-data)
;; 256ns (trie-router, pre-defined parameters)
(let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})]
(title "param") (title "param")
(assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req)))
@ -347,8 +348,9 @@
;; 66µs (trie-router, no injects) ;; 66µs (trie-router, no injects)
;; 64µs (trie-router, no injects, optimized) - 124µs (clojure) ;; 64µs (trie-router, no injects, optimized) - 124µs (clojure)
;; 63µs (trie-router, no injects, switch-case) - 124µs (clojure) ;; 63µs (trie-router, no injects, switch-case) - 124µs (clojure)
;; 63ns (trie-router, no injects, direct-data) ;; 63µs (trie-router, no injects, direct-data)
;; 54ns (trie-router, non-transient params) ;; 54µs (trie-router, non-transient params)
;; 49µs (trie-router, pre-defined parameters)
(let [requests (mapv route->req routes)] (let [requests (mapv route->req routes)]
(title "all") (title "all")
(cc/quick-bench (cc/quick-bench

View file

@ -573,6 +573,7 @@
;; 194ns (trie) ;; 194ns (trie)
;; 160ns (trie, prioritized) ;; 160ns (trie, prioritized)
;; 130ns (trie, non-transient, direct-data) ;; 130ns (trie, non-transient, direct-data)
;; 121ns (trie, pre-defined parameters)
(b! "reitit" reitit-f) (b! "reitit" reitit-f)
;; 2845ns ;; 2845ns
@ -587,12 +588,14 @@
;; 323ns (trie, prioritized) ;; 323ns (trie, prioritized)
;; 289ns (trie, prioritized, zero-copy) ;; 289ns (trie, prioritized, zero-copy)
;; 266ns (trie, non-transient, direct-data) ;; 266ns (trie, non-transient, direct-data)
;; 251ns (trie, pre-defined parameters)
(b! "reitit-ring" reitit-ring-f) (b! "reitit-ring" reitit-ring-f)
;; 385ns (java-segment-router, no injects) ;; 385ns (java-segment-router, no injects)
;; 271ms (trie) ;; 271ms (trie)
;; 240ns (trie, prioritized) ;; 240ns (trie, prioritized)
;; 214ns (trie, non-transient, direct-data) ;; 214ns (trie, non-transient, direct-data)
;; 187ns (trie, pre-defined parameters)
(b! "reitit-ring-fast" reitit-ring-fast-f) (b! "reitit-ring-fast" reitit-ring-fast-f)
;; 2553ns (linear-router) ;; 2553ns (linear-router)
@ -631,6 +634,7 @@
;; 409ns (transient) ;; 409ns (transient)
;; 409ns (staticMultiMatcher) ;; 409ns (staticMultiMatcher)
;; 305ns (non-persistent-params) ;; 305ns (non-persistent-params)
;; 293ns (pre-defined parameters)
(let [app (ring/ring-handler (ring/router opensensors-routes) {:inject-match? false, :inject-router? false}) (let [app (ring/ring-handler (ring/router opensensors-routes) {:inject-match? false, :inject-router? false})
request {:uri "/v1/users/1/devices/1", :request-method :get}] request {:uri "/v1/users/1/devices/1", :request-method :get}]
(doseq [[p r] (-> app (ring/get-router) (r/routes))] (doseq [[p r] (-> app (ring/get-router) (r/routes))]
@ -643,6 +647,7 @@
; "Elapsed time: 9183.657012 msecs" ; "Elapsed time: 9183.657012 msecs"
; "Elapsed time: 8674.70132 msecs" ; "Elapsed time: 8674.70132 msecs"
; "Elapsed time: 6714.434915 msecs" ; "Elapsed time: 6714.434915 msecs"
; "Elapsed time: 6325.310043 msecs"
(time (time
(dotimes [_ 20000000] (dotimes [_ 20000000]
(app request))) (app request)))

View file

@ -62,7 +62,7 @@
(println) (println)
(let [times (for [[path [mean lower]] (bench-routes routes req f)] (let [times (for [[path [mean lower]] (bench-routes routes req f)]
(do (do
(when verbose? (println (format "%7s %7s" mean lower) "\t" path)) (when verbose? (println (format "%7s\t%7s" mean lower) "\t" path))
[mean lower]))] [mean lower]))]
(title (str "average, lower/mean: " (title (str "average, lower/mean: "
(int (/ (reduce + (map second times)) (count times))) "/" (int (/ (reduce + (map second times)) (count times))) "/"

View file

@ -14,24 +14,24 @@
"/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}"))
(deftest tests (deftest tests
(is (= (trie/->Match {:a 1} {}) (is (= (trie/->Match {} {:a 1})
(-> (trie/insert nil "/foo" {:a 1}) (-> (trie/insert nil "/foo" {:a 1})
(trie/compile) (trie/compile)
(trie/lookup "/foo")))) (trie/lookup "/foo"))))
(is (= (trie/->Match {:a 1} {}) (is (= (trie/->Match {} {:a 1})
(-> (trie/insert nil "/foo" {:a 1}) (-> (trie/insert nil "/foo" {:a 1})
(trie/insert "/foo/*bar" {:b 1}) (trie/insert "/foo/*bar" {:b 1})
(trie/compile) (trie/compile)
(trie/lookup "/foo")))) (trie/lookup "/foo"))))
(is (= (trie/->Match {:b 1} {:bar "bar"}) (is (= (trie/->Match {:bar "bar"} {:b 1})
(-> (trie/insert nil "/foo" {:a 1}) (-> (trie/insert nil "/foo" {:a 1})
(trie/insert "/foo/*bar" {:b 1}) (trie/insert "/foo/*bar" {:b 1})
(trie/compile) (trie/compile)
(trie/lookup "/foo/bar")))) (trie/lookup "/foo/bar"))))
(is (= (trie/->Match {:a 1} {}) (is (= (trie/->Match {} {:a 1})
(-> (trie/insert nil "" {:a 1}) (-> (trie/insert nil "" {:a 1})
(trie/compile) (trie/compile)
(trie/lookup ""))))) (trie/lookup "")))))