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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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