From f2d131a897230d5485949ab8b5f28f985db644fe Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 26 Jan 2019 16:03:44 +0200 Subject: [PATCH 01/51] wip --- examples/ring-swagger/src/example/server.clj | 3 +- modules/reitit-core/java-src/reitit/Trie.java | 216 ++++++++++++++++++ modules/reitit-core/project.clj | 2 +- modules/reitit-core/src/reitit/core.cljc | 7 +- modules/reitit-core/src/reitit/trie.cljc | 146 ++++++++++++ perf-test/clj/reitit/bide_perf_test.clj | 66 ++++++ .../clj/reitit/opensensors_perf_test.clj | 21 +- .../clj/reitit/prefix_tree_perf_test.clj | 25 +- project.clj | 3 +- 9 files changed, 469 insertions(+), 20 deletions(-) create mode 100644 modules/reitit-core/java-src/reitit/Trie.java create mode 100644 modules/reitit-core/src/reitit/trie.cljc diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj index fb8e869d..c79f0db2 100644 --- a/examples/ring-swagger/src/example/server.clj +++ b/examples/ring-swagger/src/example/server.clj @@ -83,7 +83,8 @@ (ring/routes (swagger-ui/create-swagger-ui-handler {:path "/" - :config {:validatorUrl nil}}) + :config {:validatorUrl nil + :operationsSorter "alpha"}}) (ring/create-default-handler)))) (defn start [] 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..4dfbae2b --- /dev/null +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -0,0 +1,216 @@ +package reitit; + +// https://www.codeproject.com/Tips/1190293/Iteration-Over-Java-Collections-with-High-Performa + +import clojure.lang.Keyword; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.*; + +public class Trie { + + private static String decode(char[] chars, int i, int j, boolean hasPercent, boolean hasPlus) { + final String s = new String(chars, i, j); + try { + if (hasPercent) { + return URLDecoder.decode(hasPlus ? s.replace("+", "%2B") : s, "UTF-8"); + } + } catch (UnsupportedEncodingException ignored) { + } + return s; + } + + public static class Match { + public final List params = new ArrayList<>(); + 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(); + } + } + + public static class Path { + final char[] value; + final int size; + + Path(String value) { + this.value = value.toCharArray(); + this.size = value.length(); + } + } + + public interface Matcher { + Match match(int i, Path path, Match match); + } + + public static StaticMatcher staticMatcher(String path, Matcher child) { + return new StaticMatcher(path, child); + } + + static class StaticMatcher implements Matcher { + private final Matcher child; + private final char[] path; + private final int size; + + StaticMatcher(String path, Matcher child) { + this.path = path.toCharArray(); + this.size = path.length(); + this.child = child; + } + + @Override + public Match match(int i, Path path, Match match) { + final char[] value = path.value; + if (path.size < i + size) { + return null; + } + for (int j = 0; j < size; j++) { + if (value[j + i] != this.path[j]) { + return null; + } + } + return child.match(i + size, path, match); + } + + @Override + public String toString() { + return "[\"" + new String(path) + "\" " + child + "]"; + } + } + + public static DataMatcher dataMatcher(Object data) { + return new DataMatcher(data); + } + + static final class DataMatcher implements Matcher { + private final Object data; + + DataMatcher(Object data) { + this.data = data; + } + + @Override + public Match match(int i, Path path, Match match) { + if (i == path.size) { + match.data = data; + return match; + } + return null; + } + + @Override + public String toString() { + return (data != null ? data.toString() : "nil"); + } + } + + public static WildMatcher wildMatcher(Keyword parameter, Matcher child) { + return new WildMatcher(parameter, child); + } + + static final class WildMatcher implements Matcher { + private final Keyword key; + private final Matcher child; + + WildMatcher(Keyword key, Matcher child) { + this.key = key; + this.child = child; + } + + @Override + public Match match(int i, Path path, Match match) { + final char[] value = path.value; + if (i < path.size && value[i] != '/') { + boolean hasPercent = false; + boolean hasPlus = false; + for (int j = i; j < path.size; j++) { + if (value[j] == '/') { + final Match m = child.match(j, path, match); + if (m != null) { + m.params.add(key); + m.params.add(decode(value, i, j - i, hasPercent, hasPlus)); + } + return m; + } else if (value[j] == '%') { + hasPercent = true; + } else if (value[j] == '+') { + hasPlus = true; + } + } + if (child instanceof DataMatcher) { + final Match m = child.match(path.size, path, match); + m.params.add(key); + m.params.add(decode(value, i, path.size - i, hasPercent, hasPlus)); + return m; + } + } + return null; + } + + @Override + public String toString() { + return "[" + key + " " + child + "]"; + } + } + + public static LinearMatcher linearMatcher(List childs) { + return new LinearMatcher(childs); + } + + static final class LinearMatcher implements Matcher { + + private final Matcher[] childs; + private final int size; + + LinearMatcher(List childs) { + this.childs = childs.toArray(new Matcher[0]); + this.size = childs.size(); + } + + @Override + public Match match(int i, Path path, Match match) { + for (int j = 0; j < size; j++) { + final Match m = childs[j].match(i, path, match); + if (m != null) { + return m; + } + } + return null; + } + + @Override + public String toString() { + return Arrays.toString(childs); + } + } + + public static Object lookup(Matcher matcher, String path) { + return matcher.match(0, new Path(path), new Match()); + } + + public static void main(String[] args) { + + //Matcher matcher = new StaticMatcher("/kikka", new StaticMatcher("/kukka", new DataMatcher(1))); +// Matcher matcher = +// staticMatcher("/kikka/", +// wildMatcher(Keyword.intern("kukka"), +// staticMatcher("/kikka", +// dataMatcher(1)))); + Matcher matcher = + linearMatcher( + Arrays.asList( + staticMatcher("/auth/", + linearMatcher( + Arrays.asList( + staticMatcher("login", dataMatcher(1)), + staticMatcher("recovery", dataMatcher(2))))))); + System.err.println(matcher); + System.out.println(lookup(matcher, "/auth/login")); + System.out.println(lookup(matcher, "/auth/recovery")); + } +} diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index afcb84b8..4f04c95a 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -8,5 +8,5 @@ :plugins [[lein-parent "0.3.2"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} - :java-source-paths ["java-src"] + ;: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 b905915b..5df916af 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -2,6 +2,7 @@ (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] [reitit.segment :as segment] + [reitit.segment :as trie] [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) #?(:clj (:import (reitit.impl Route)))) @@ -265,11 +266,11 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result % path-params))] - [(segment/insert pl p (->Match p data result nil nil)) + [(trie/insert pl p (->Match p data result nil nil)) (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) - pl (segment/compile pl) + pl (trie/compile pl) lookup (impl/fast-map nl) routes (uncompile-routes compiled-routes)] ^{:type ::router} @@ -286,7 +287,7 @@ (route-names [_] names) (match-by-path [_ path] - (if-let [match (segment/lookup pl path)] + (if-let [match (trie/lookup pl path)] (-> (:data match) (assoc :path-params (:path-params match)) (assoc :path path)))) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc new file mode 100644 index 00000000..1fa98100 --- /dev/null +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -0,0 +1,146 @@ +(ns reitit.trie + (:refer-clojure :exclude [compile]) + (:require [clojure.string :as str]) + (:import [reitit Trie Trie$Match Trie$Matcher])) + +(defrecord Match [data path-params]) +(defrecord Node [children wilds catch-all data]) + +;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix +(defn- -common-prefix [s1 s2] + (let [max (min (count s1) (count s2))] + (loop [i 0] + (cond + ;; full match + (> i max) + (subs s1 0 max) + ;; partial match + (not= (get s1 i) (get s2 i)) + (if-not (zero? i) (subs s1 0 i)) + ;; recur + :else + (recur (inc i)))))) + +(defn- -keyword [s] + (if-let [i (str/index-of s "/")] + (keyword (subs s 0 i) (subs s (inc i))) + (keyword s))) + +(defn- -split [s] + (let [-static (fn [from to] (if-not (= from to) [(subs s from to)])) + -wild (fn [from to] [(-keyword (subs s (inc from) to))]) + -catch-all (fn [from to] [#{(keyword (subs s (inc from) to))}])] + (loop [ss nil, from 0, to 0] + (if (= to (count s)) + (concat ss (-static from to)) + (condp = (get s to) + \{ (let [to' (or (str/index-of s "}" to) (throw (ex-info (str "Unbalanced brackets: " (pr-str s)) {})))] + (recur (concat ss (-static from to) (-wild to to')) (inc to') (inc to'))) + \: (let [to' (or (str/index-of s "/" to) (count s))] + (recur (concat ss (-static from to) (-wild to to')) to' to')) + \* (let [to' (count s)] + (recur (concat ss (-static from to) (-catch-all to to')) to' to')) + (recur ss from (inc to))))))) + +(defn- -node [m] + (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) + +(defn- -insert [node [path & ps] data] + (let [node' (cond + + (nil? path) + (assoc node :data data) + + (keyword? path) + (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))) + + (set? path) + (assoc-in node [:catch-all path] (-node {:data data})) + + (str/blank? path) + (-insert node ps data) + + :else + (or + (reduce + (fn [_ [p n]] + (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)] + (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)] + (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))))] + (if-let [child (get-in node' [:children ""])] + ;; optimize by removing empty paths + (-> (merge-with merge node' child) + (update :children dissoc "")) + node'))) + +(defn insert [node path data] + (-insert (or node (-node {})) (-split path) data)) + +(defn ^Trie$Matcher compile [{:keys [data children wilds catch-all]}] + (let [matchers (cond-> [] + data (conj (Trie/dataMatcher data)) + children (into (for [[p c] children] (Trie/staticMatcher p (compile c)))) + wilds (into (for [[p c] wilds] (Trie/wildMatcher p (compile c)))))] + (if (rest matchers) + (Trie/linearMatcher matchers) + (first matchers)))) + +(defn pretty [{:keys [data children wilds catch-all]}] + (into + (if data [data] []) + (mapcat (fn [[p n]] [p (pretty n)]) (concat children wilds catch-all)))) + +(defn lookup [^Trie$Matcher matcher path] + (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] + (->Match (.data match) (clojure.lang.PersistentHashMap/create (.toArray (.params match)))))) + +;; +;; matcher +;; + +;; +;; spike +;; + +(-> nil + (insert "/:abba" 1) + (insert "/kikka" 2) + (insert "/kikka/kakka/kukka" 3) + (insert "/kikka/:kakka/kukka" 4) + (insert "/kikka/kuri/{user/doc}.html" 5) + (insert "/kikkare/*path" 6) + #_(pretty)) + +(-> nil + (insert "/kikka" 2) + (insert "/kikka/kakka/kukka" 3) + (insert "/kikka/:kakka/kurkku" 4) + (insert "/kikka/kuri/{user/doc}/html" 5) + (compile) + (lookup "/kikka/kakka/kurkku")) + +;; => + +["/" + ["kikka" [2 + "/" ["k" ["akka/kukka" [3] + "uri/" [:user/doc [".html" [5]]]] + :kakka ["/kukka" [4]]] + "re/" [#{:path} [6]]] + :abba [1]]] + + diff --git a/perf-test/clj/reitit/bide_perf_test.clj b/perf-test/clj/reitit/bide_perf_test.clj index b96d971d..885a32bc 100644 --- a/perf-test/clj/reitit/bide_perf_test.clj +++ b/perf-test/clj/reitit/bide_perf_test.clj @@ -205,3 +205,69 @@ (routing-test1) (routing-test2) (reverse-routing-test)) + +(import '[reitit Trie]) + +(set! *warn-on-reflection* true) + +(comment + (let [trie ] + + + (println + (Trie/lookup trie "/auth/login")) + + ;; 27ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/auth/login"))) + + (println + (Trie/lookup trie "/auth/recovery/token/123")) + + ;; 82ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/auth/recovery/token/123"))) + + (println + (Trie/lookup trie "/workspace/1/1")) + + ;; 96ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/workspace/1/1"))))) + +(comment + (let [trie (Trie/linearMatcher + [(Trie/staticMatcher + "/auth/" (Trie/linearMatcher + [(Trie/staticMatcher "login" (Trie/dataMatcher 1)) + (Trie/staticMatcher "recovery/token/" (Trie/wildMatcher :token (Trie/dataMatcher 2)))])) + (Trie/staticMatcher + "/workspace/" (Trie/wildMatcher :project (Trie/staticMatcher "/" (Trie/wildMatcher :page (Trie/dataMatcher 3)))))])] + + + (println + (Trie/lookup trie "/auth/login")) + + ;; 27ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/auth/login"))) + + (println + (Trie/lookup trie "/auth/recovery/token/123")) + + ;; 82ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/auth/recovery/token/123"))) + + (println + (Trie/lookup trie "/workspace/1/1")) + + ;; 96ns + (cc/quick-bench + (dotimes [_ 1000] + (Trie/lookup trie "/workspace/1/1"))))) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 427d9b2e..f8ce7900 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -578,35 +578,36 @@ ;; 806ns (decode path-parameters) ;; 735ns (maybe-map-values) ;; 474ns (java-segment-router) - (b! "reitit-ring" reitit-ring-f) + #_(b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) - (b! "reitit-ring-fast" reitit-ring-fast-f) + #_(b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) - (b! "reitit-ring-linear" reitit-ring-linear-f) + #_(b! "reitit-ring-linear" reitit-ring-linear-f) ;; 2137ns - (b! "calfpath-walker" calfpath-walker-f) + #_(b! "calfpath-walker" calfpath-walker-f) ;; 4774ns - (b! "calfpath-unroll" calfpath-unroll-f) + #_(b! "calfpath-unroll" calfpath-unroll-f) ;; 2821ns - (b! "pedestal" pedestal-f) + #_(b! "pedestal" pedestal-f) ;; 4803ns - (b! "calfpath-macros" calfpath-macros-f) + #_(b! "calfpath-macros" calfpath-macros-f) ;; 11615ns - (b! "compojure" compojure-f) + #_(b! "compojure" compojure-f) ;; 15034ns - (b! "bidi" bidi-f) + #_(b! "bidi" bidi-f) ;; 19688ns - (b! "ataraxy" ataraxy-f))) + #_(b! "ataraxy" ataraxy-f))) (comment (bench-rest!)) + diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index aa266152..b56f2c58 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -2,6 +2,7 @@ (:require [clojure.test :refer :all] [io.pedestal.http.route.prefix-tree :as p] [reitit.segment :as segment] + [reitit.trie :as trie] [criterium.core :as cc]) (:import (reitit SegmentTrie))) @@ -70,7 +71,7 @@ (p/insert acc p d)) nil routes)) -(def matcher +(def segment-matcher (.matcher ^SegmentTrie (reduce @@ -78,6 +79,13 @@ (segment/insert acc p d)) nil routes))) +(def trie-matcher + (trie/compile + (reduce + (fn [acc [p d]] + (trie/insert acc p d)) + nil routes))) + (defn bench! [] ;; 2.3µs @@ -110,12 +118,21 @@ ;; 0.51µs (Cleanup) ;; 0.30µs (Java) (cc/quick-bench - (segment/lookup matcher "/v1/orgs/1/topics"))) + (segment/lookup segment-matcher "/v1/orgs/1/topics")) + + ;; 0.32µs (initial) + ;; 0.30µs (iterate arrays) + ;; 0.28µs (list-params) + (cc/quick-bench + (trie/lookup trie-matcher "/v1/orgs/1/topics"))) (comment (bench!)) +(set! *warn-on-reflection* true) + (comment (p/lookup pedestal-tree "/v1/orgs/1/topics") - #_(trie/lookup reitit-tree "/v1/orgs/1/topics" {}) - (segment/lookup matcher "/v1/orgs/1/topics")) + (trie/lookup trie-matcher "/v1/orgs/1/topics") + (segment/lookup segment-matcher "/v1/orgs/1/topics")) + diff --git a/project.clj b/project.clj index 19ea2c08..f67ae638 100644 --- a/project.clj +++ b/project.clj @@ -38,6 +38,7 @@ [io.pedestal/pedestal.service "0.5.5"]] :plugins [[jonase/eastwood "0.3.4"] + [lein-virgil "0.1.7"] [lein-doo "0.1.11"] [lein-cljsbuild "1.1.7"] [lein-cloverage "1.0.13"] @@ -61,7 +62,7 @@ "modules/reitit-sieppari/src" "modules/reitit-pedestal/src"] - :java-source-paths ["modules/reitit-core/java-src"] + ;:java-source-paths ["modules/reitit-core/java-src"] :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/clojurescript "1.10.439"] From fe0ea19e31acc3b92b1eb28c8fc4825fc2736eda Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 27 Jan 2019 18:57:29 +0200 Subject: [PATCH 02/51] Better perf with transient parameters --- modules/reitit-core/java-src/reitit/Trie.java | 23 +++++++++++-------- modules/reitit-core/src/reitit/core.cljc | 2 +- modules/reitit-core/src/reitit/trie.cljc | 2 +- .../clj/reitit/opensensors_perf_test.clj | 23 +++++++++++++++++-- project.clj | 2 ++ 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 4dfbae2b..aa12c957 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -2,7 +2,10 @@ package reitit; // https://www.codeproject.com/Tips/1190293/Iteration-Over-Java-Collections-with-High-Performa +import clojure.lang.IPersistentMap; +import clojure.lang.ITransientMap; import clojure.lang.Keyword; +import clojure.lang.PersistentArrayMap; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -22,14 +25,18 @@ public class Trie { } public static class Match { - public final List params = new ArrayList<>(); + final ITransientMap params = PersistentArrayMap.EMPTY.asTransient(); public Object data; + public IPersistentMap parameters() { + return params.persistent(); + } + @Override public String toString() { Map m = new HashMap<>(); m.put(Keyword.intern("data"), data); - m.put(Keyword.intern("params"), params); + m.put(Keyword.intern("params"), params.persistent()); return m.toString(); } } @@ -132,8 +139,7 @@ public class Trie { if (value[j] == '/') { final Match m = child.match(j, path, match); if (m != null) { - m.params.add(key); - m.params.add(decode(value, i, j - i, hasPercent, hasPlus)); + m.params.assoc(key, decode(value, i, j - i, hasPercent, hasPlus)); } return m; } else if (value[j] == '%') { @@ -142,12 +148,11 @@ public class Trie { hasPlus = true; } } - if (child instanceof DataMatcher) { - final Match m = child.match(path.size, path, match); - m.params.add(key); - m.params.add(decode(value, i, path.size - i, hasPercent, hasPlus)); - return m; + final Match m = child.match(path.size, path, match); + if (m != null) { + m.params.assoc(key, decode(value, i, path.size - i, hasPercent, hasPlus)); } + return m; } return null; } diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 5df916af..d6cc99b1 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -2,7 +2,7 @@ (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] [reitit.segment :as segment] - [reitit.segment :as trie] + [reitit.trie :as trie] [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) #?(:clj (:import (reitit.impl Route)))) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 1fa98100..f615a597 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -106,7 +106,7 @@ (defn lookup [^Trie$Matcher matcher path] (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] - (->Match (.data match) (clojure.lang.PersistentHashMap/create (.toArray (.params match)))))) + (->Match (.data match) (.parameters match)))) ;; ;; matcher diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index f8ce7900..e742422d 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -568,6 +568,7 @@ ;; 662ns (prefix-tree-router) ;; 567ns (segment-router) ;; 326ns (java-segment-router) + ;; 194ms (trie) (b! "reitit" reitit-f) ;; 2845ns @@ -578,10 +579,12 @@ ;; 806ns (decode path-parameters) ;; 735ns (maybe-map-values) ;; 474ns (java-segment-router) - #_(b! "reitit-ring" reitit-ring-f) + ;; 373ms (trie) + (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) - #_(b! "reitit-ring-fast" reitit-ring-fast-f) + ;; 271ms (trie) + (b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) @@ -611,3 +614,19 @@ (comment (bench-rest!)) +(set! *warn-on-reflection* true) + +(require '[clj-async-profiler.core :as prof]) + +(comment + ;; 629ms (arraylist) + ;; 395ns (transient) + (let [app (ring/ring-handler (ring/router opensensors-routes))] + (doseq [[p r] (-> app (ring/get-router) (r/routes))] + (when-not (app {:uri p, :request-method :get}) + (println "FAIL:" p))) + (println (app {:uri "/v1/users/1/devices/1", :request-method :get})) + (prof/start {}) + (dotimes [_ 100000] + (app {:uri "/v1/users/1/devices/1", :request-method :get})) + (str (prof/stop {})))) diff --git a/project.clj b/project.clj index f67ae638..b9e2b8b4 100644 --- a/project.clj +++ b/project.clj @@ -91,6 +91,8 @@ [manifold "0.1.8"] [funcool/promesa "1.9.0"] + [com.clojure-goes-fast/clj-async-profiler "0.2.2"] + ;; https://github.com/bensu/doo/issues/180 [fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]} :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} From eaee4ca38d5a50f51850e141200d014ccf000bb3 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 27 Jan 2019 22:10:46 +0200 Subject: [PATCH 03/51] Implement catch-all from trie --- modules/reitit-core/java-src/reitit/Trie.java | 53 +++++++++++++++++-- modules/reitit-core/src/reitit/core.cljc | 7 ++- modules/reitit-core/src/reitit/trie.cljc | 6 ++- project.clj | 2 +- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index aa12c957..55250fdc 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -13,8 +13,8 @@ import java.util.*; public class Trie { - private static String decode(char[] chars, int i, int j, boolean hasPercent, boolean hasPlus) { - final String s = new String(chars, i, j); + private static String decode(char[] chars, int offset, int count, boolean hasPercent, boolean hasPlus) { + final String s = new String(chars, offset, count); try { if (hasPercent) { return URLDecoder.decode(hasPlus ? s.replace("+", "%2B") : s, "UTF-8"); @@ -24,6 +24,20 @@ public class Trie { return s; } + private static String decode(char[] chars, int offset, int count) { + boolean hasPercent = false; + boolean hasPlus = false; + for (int j = offset; j < offset + count; j++) { + if (chars[j] == '%') { + hasPercent = true; + } else if (chars[j] == '+') { + hasPlus = true; + } + } + System.err.println(); + return decode(chars, offset, count, hasPercent, hasPlus); + } + public static class Match { final ITransientMap params = PersistentArrayMap.EMPTY.asTransient(); public Object data; @@ -36,7 +50,7 @@ public class Trie { public String toString() { Map m = new HashMap<>(); m.put(Keyword.intern("data"), data); - m.put(Keyword.intern("params"), params.persistent()); + m.put(Keyword.intern("params"), parameters()); return m.toString(); } } @@ -163,6 +177,35 @@ public class Trie { } } + public static CatchAllMatcher catchAllMatcher(Keyword parameter, Object data) { + return new CatchAllMatcher(parameter, data); + } + + 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, Path path, Match match) { + if (i < path.value.length) { + match.params.assoc(parameter, decode(path.value, i, path.size - i)); + match.data = data; + return match; + } + return null; + } + + @Override + public String toString() { + return "[" + parameter + " " + new DataMatcher(data) + "]"; + } + } + public static LinearMatcher linearMatcher(List childs) { return new LinearMatcher(childs); } @@ -198,6 +241,10 @@ public class Trie { return matcher.match(0, new Path(path), new Match()); } + public static Matcher scanner(List matchers) { + return new LinearMatcher(matchers); + } + public static void main(String[] args) { //Matcher matcher = new StaticMatcher("/kikka", new StaticMatcher("/kukka", new DataMatcher(1))); diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index d6cc99b1..207382a1 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,7 +1,6 @@ (ns reitit.core (:require [meta-merge.core :refer [meta-merge]] [clojure.string :as str] - [reitit.segment :as segment] [reitit.trie :as trie] [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) #?(:clj @@ -176,12 +175,12 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result % path-params))] - [(conj pl (-> (segment/insert nil p (->Match p data result nil nil)) (segment/compile))) + [(conj pl (-> (trie/insert nil p (->Match p data result nil nil)) (trie/compile))) (if name (assoc nl name f) nl)])) [[] {}] compiled-routes) lookup (impl/fast-map nl) - scanner (segment/scanner pl) + scanner (trie/scanner pl) routes (uncompile-routes compiled-routes)] ^{:type ::router} (reify @@ -197,7 +196,7 @@ (route-names [_] names) (match-by-path [_ path] - (if-let [match (segment/lookup scanner path)] + (if-let [match (trie/lookup scanner path)] (-> (:data match) (assoc :path-params (:path-params match)) (assoc :path path)))) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index f615a597..7ef8e481 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -94,7 +94,8 @@ (let [matchers (cond-> [] data (conj (Trie/dataMatcher data)) children (into (for [[p c] children] (Trie/staticMatcher p (compile c)))) - wilds (into (for [[p c] wilds] (Trie/wildMatcher p (compile c)))))] + wilds (into (for [[p c] wilds] (Trie/wildMatcher p (compile c)))) + catch-all (into (for [[p c] catch-all] (Trie/catchAllMatcher (first p) (:data c)))))] (if (rest matchers) (Trie/linearMatcher matchers) (first matchers)))) @@ -108,6 +109,9 @@ (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] (->Match (.data match) (.parameters match)))) +(defn scanner [compiled-tries] + (Trie/scanner compiled-tries)) + ;; ;; matcher ;; diff --git a/project.clj b/project.clj index b9e2b8b4..bd289aa4 100644 --- a/project.clj +++ b/project.clj @@ -62,7 +62,7 @@ "modules/reitit-sieppari/src" "modules/reitit-pedestal/src"] - ;:java-source-paths ["modules/reitit-core/java-src"] + :java-source-paths ["modules/reitit-core/java-src"] :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/clojurescript "1.10.439"] From 08edbe5b0126a5358d087b47d2aee20e4bcbcb66 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 28 Jan 2019 09:04:47 +0200 Subject: [PATCH 04/51] . --- modules/reitit-core/project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index 4f04c95a..afcb84b8 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -8,5 +8,5 @@ :plugins [[lein-parent "0.3.2"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} - ;:java-source-paths ["java-src"] + :java-source-paths ["java-src"] :dependencies [[meta-merge]]) From 94f4ab4e753af01c9b3635c067f437fcbe367ba7 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 07:27:45 +0200 Subject: [PATCH 05/51] SNAPSHOT to avoid reflection --- .../clj/reitit/opensensors_perf_test.clj | 162 +++++++++--------- project.clj | 2 +- 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index e742422d..92ae6e57 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -14,7 +14,8 @@ [io.pedestal.http.route.definition.table :as table] [io.pedestal.http.route.map-tree :as map-tree] [io.pedestal.http.route.router :as pedestal] - [reitit.core :as r])) + [reitit.core :as r] + [criterium.core :as cc])) ;; ;; start repl with `lein perf repl` @@ -235,78 +236,78 @@ (def opensensors-compojure-routes (routes (context "/v1" [] - (context "/public" [] - (ANY "/topics/:topic" [] {:name :test/route4} handler) - (ANY "/users/:user-id" [] {:name :test/route16} handler) - (ANY "/orgs/:org-id" [] {:name :test/route18} handler)) - (context "/users/:user-id" [] - (ANY "/orgs/:org-id" [] {:name :test/route5} handler) - (ANY "/invitations" [] {:name :test/route7} handler) - (ANY "/topics" [] {:name :test/route9} handler) - (ANY "/bookmarks/followers" [] {:name :test/route10} handler) - (context "/devices" [] - (ANY "/" [] {:name :test/route15} handler) - #_(ANY "/bulk" [] {:name :test/route21} handler) - (ANY "/:client-id" [] {:name :test/route35} handler) - (ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) - (ANY "/device-errors" [] {:name :test/route22} handler) - (ANY "/usage-stats" [] {:name :test/route24} handler) - (ANY "/claim-device/:client-id" [] {:name :test/route26} handler) - (ANY "/owned-orgs" [] {:name :test/route31} handler) - (ANY "/bookmark/:topic" [] {:name :test/route33} handler) - (ANY "/" [] {:name :test/route36} handler) - (ANY "/orgs" [] {:name :test/route52} handler) - (ANY "/api-key" [] {:name :test/route43} handler) - (ANY "/bookmarks" [] {:name :test/route56} handler)) - (ANY "/search/topics/:term" [] {:name :test/route6} handler) - (context "/orgs" [] - (ANY "/" [] {:name :test/route55} handler) - (context "/:org-id" [] - (context "/devices" [] - (ANY "/" [] {:name :test/route37} handler) - (ANY "/:device-id" [] {:name :test/route13} handler) - #_(ANY "/:batch/:type" [] {:name :test/route8} handler)) - (ANY "/usage-stats" [] {:name :test/route12} handler) - (ANY "/invitations" [] {:name :test/route19} handler) - (context "/members" [] - (ANY "/:user-id" [] {:name :test/route34} handler) - (ANY "/" [] {:name :test/route38} handler) - #_(ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) - (ANY "/errors" [] {:name :test/route17} handler) - (ANY "/" [] {:name :test/route42} handler) - (ANY "/confirm-membership/:token" [] {:name :test/route46} handler) - (ANY "/topics" [] {:name :test/route57} handler))) - (context "/messages" [] - (ANY "/user/:user-id" [] {:name :test/route14} handler) - (ANY "/device/:client-id" [] {:name :test/route30} handler) - (ANY "/topic/:topic" [] {:name :test/route48} handler)) - (context "/topics" [] - (ANY "/:topic" [] {:name :test/route32} handler) - (ANY "/" [] {:name :test/route54} handler)) - (ANY "/whoami" [] {:name :test/route41} handler) - (ANY "/login" [] {:name :test/route51} handler)) + (context "/public" [] + (ANY "/topics/:topic" [] {:name :test/route4} handler) + (ANY "/users/:user-id" [] {:name :test/route16} handler) + (ANY "/orgs/:org-id" [] {:name :test/route18} handler)) + (context "/users/:user-id" [] + (ANY "/orgs/:org-id" [] {:name :test/route5} handler) + (ANY "/invitations" [] {:name :test/route7} handler) + (ANY "/topics" [] {:name :test/route9} handler) + (ANY "/bookmarks/followers" [] {:name :test/route10} handler) + (context "/devices" [] + (ANY "/" [] {:name :test/route15} handler) + #_(ANY "/bulk" [] {:name :test/route21} handler) + (ANY "/:client-id" [] {:name :test/route35} handler) + (ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) + (ANY "/device-errors" [] {:name :test/route22} handler) + (ANY "/usage-stats" [] {:name :test/route24} handler) + (ANY "/claim-device/:client-id" [] {:name :test/route26} handler) + (ANY "/owned-orgs" [] {:name :test/route31} handler) + (ANY "/bookmark/:topic" [] {:name :test/route33} handler) + (ANY "/" [] {:name :test/route36} handler) + (ANY "/orgs" [] {:name :test/route52} handler) + (ANY "/api-key" [] {:name :test/route43} handler) + (ANY "/bookmarks" [] {:name :test/route56} handler)) + (ANY "/search/topics/:term" [] {:name :test/route6} handler) + (context "/orgs" [] + (ANY "/" [] {:name :test/route55} handler) + (context "/:org-id" [] + (context "/devices" [] + (ANY "/" [] {:name :test/route37} handler) + (ANY "/:device-id" [] {:name :test/route13} handler) + #_(ANY "/:batch/:type" [] {:name :test/route8} handler)) + (ANY "/usage-stats" [] {:name :test/route12} handler) + (ANY "/invitations" [] {:name :test/route19} handler) + (context "/members" [] + (ANY "/:user-id" [] {:name :test/route34} handler) + (ANY "/" [] {:name :test/route38} handler) + #_(ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) + (ANY "/errors" [] {:name :test/route17} handler) + (ANY "/" [] {:name :test/route42} handler) + (ANY "/confirm-membership/:token" [] {:name :test/route46} handler) + (ANY "/topics" [] {:name :test/route57} handler))) + (context "/messages" [] + (ANY "/user/:user-id" [] {:name :test/route14} handler) + (ANY "/device/:client-id" [] {:name :test/route30} handler) + (ANY "/topic/:topic" [] {:name :test/route48} handler)) + (context "/topics" [] + (ANY "/:topic" [] {:name :test/route32} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/whoami" [] {:name :test/route41} handler) + (ANY "/login" [] {:name :test/route51} handler)) (context "/v2" [] - (ANY "/whoami" [] {:name :test/route1} handler) - (context "/users/:user-id" [] - (ANY "/datasets" [] {:name :test/route2} handler) - (ANY "/devices" [] {:name :test/route25} handler) - (context "/topics" [] - (ANY "/bulk" [] {:name :test/route29} handler) - (ANY "/" [] {:name :test/route54} handler)) - (ANY "/" [] {:name :test/route45} handler)) - (context "/public" [] - (context "/projects/:project-id" [] - (ANY "/datasets" [] {:name :test/route3} handler) - (ANY "/" [] {:name :test/route27} handler)) - #_(ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) - (ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) - (ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) - (ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) - (ANY "/login" [] {:name :test/route23} handler) - (ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) - (ANY "/schemas" [] {:name :test/route44} handler) - (ANY "/topics/:topic" [] {:name :test/route47} handler) - (ANY "/topics" [] {:name :test/route50} handler)))) + (ANY "/whoami" [] {:name :test/route1} handler) + (context "/users/:user-id" [] + (ANY "/datasets" [] {:name :test/route2} handler) + (ANY "/devices" [] {:name :test/route25} handler) + (context "/topics" [] + (ANY "/bulk" [] {:name :test/route29} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/" [] {:name :test/route45} handler)) + (context "/public" [] + (context "/projects/:project-id" [] + (ANY "/datasets" [] {:name :test/route3} handler) + (ANY "/" [] {:name :test/route27} handler)) + #_(ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) + (ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) + (ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) + (ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) + (ANY "/login" [] {:name :test/route23} handler) + (ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) + (ANY "/schemas" [] {:name :test/route44} handler) + (ANY "/topics/:topic" [] {:name :test/route47} handler) + (ANY "/topics" [] {:name :test/route50} handler)))) (def opensensors-pedestal-routes (map-tree/router @@ -621,12 +622,19 @@ (comment ;; 629ms (arraylist) ;; 395ns (transient) - (let [app (ring/ring-handler (ring/router opensensors-routes))] + ;; (staticMultiMatcher) + (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))] (when-not (app {:uri p, :request-method :get}) (println "FAIL:" p))) - (println (app {:uri "/v1/users/1/devices/1", :request-method :get})) - (prof/start {}) - (dotimes [_ 100000] - (app {:uri "/v1/users/1/devices/1", :request-method :get})) + (println (app request)) + (cc/quick-bench + (app request)) + #_#_#_(prof/start {}) + ; "Elapsed time: 9183.657012 msecs" + ; "Elapsed time: 8674.70132 msecs" + (time + (dotimes [_ 20000000] + (app request))) (str (prof/stop {})))) diff --git a/project.clj b/project.clj index bd289aa4..0b3fa534 100644 --- a/project.clj +++ b/project.clj @@ -91,7 +91,7 @@ [manifold "0.1.8"] [funcool/promesa "1.9.0"] - [com.clojure-goes-fast/clj-async-profiler "0.2.2"] + [com.clojure-goes-fast/clj-async-profiler "0.2.3-SNAPSHOT"] ;; https://github.com/bensu/doo/issues/180 [fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]} From 415cd7af89d71272f16f411aa64f3b71c3228af6 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 07:51:28 +0200 Subject: [PATCH 06/51] Test the StaticMultiMatcher - no big difference --- 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 92ae6e57..1f7465f7 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -621,8 +621,8 @@ (comment ;; 629ms (arraylist) - ;; 395ns (transient) - ;; (staticMultiMatcher) + ;; 409ns (transient) + ;; 409ns (staticMultiMatcher) (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))] @@ -631,7 +631,7 @@ (println (app request)) (cc/quick-bench (app request)) - #_#_#_(prof/start {}) + (prof/start {}) ; "Elapsed time: 9183.657012 msecs" ; "Elapsed time: 8674.70132 msecs" (time From ce80f83319282a359c92641f360653c69dd9a062 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 16:41:15 +0200 Subject: [PATCH 07/51] :segment-router -> :trie-router --- doc/advanced/different_routers.md | 4 ++-- modules/reitit-core/src/reitit/core.cljc | 12 ++++++------ modules/reitit-core/src/reitit/trie.cljc | 4 +++- test/cljc/reitit/core_test.cljc | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/doc/advanced/different_routers.md b/doc/advanced/different_routers.md index 24f205b4..aa7b78ae 100644 --- a/doc/advanced/different_routers.md +++ b/doc/advanced/different_routers.md @@ -5,10 +5,10 @@ Reitit ships with several different implementations for the `Router` protocol, o | router | description | | ------------------------------|-------------| | `:linear-router` | Matches the routes one-by-one starting from the top until a match is found. Slow, but works with all route trees. -| `:segment-router` | Router that creates a optimized [search trie](https://en.wikipedia.org/wiki/Trie) out of an route table. Much faster than `:linear-router` for wildcard routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). +| `:trie-router` | Router that creates a optimized [search trie](https://en.wikipedia.org/wiki/Trie) out of an route table. Much faster than `:linear-router` for wildcard routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). | `:lookup-router` | Fast router, uses hash-lookup to resolve the route. Valid if no paths have path or catch-all parameters and there are no [Route conflicts](../basics/route_conflicts.md). | `:single-static-path-router` | Super fast router: string-matches a route. Valid only if there is one static route. -| `:mixed-router` | Contains two routers: `:segment-router` for wildcard routes and a `:lookup-router` or `:single-static-path-router` for static routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). +| `:mixed-router` | Contains two routers: `:trie-router` for wildcard routes and a `:lookup-router` or `:single-static-path-router` for static routes. Valid only if there are no [Route conflicts](../basics/route_conflicts.md). | `:quarantine-router` | Contains two routers: `:mixed-router` for non-conflicting routes and a `:linear-router` for conflicting routes. The router name can be asked from the router: diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 207382a1..307ba91e 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -252,11 +252,11 @@ (if-let [match (impl/fast-get lookup name)] (match (impl/path-params path-params)))))))) -(defn segment-router - "Creates a special prefix-tree style segment router from resolved routes and optional +(defn trie-router + "Creates a special prefix-tree router from resolved routes and optional expanded options. See [[router]] for available options." ([compiled-routes] - (segment-router compiled-routes {})) + (trie-router compiled-routes {})) ([compiled-routes opts] (let [names (find-names compiled-routes opts) [pl nl] (reduce @@ -276,7 +276,7 @@ (reify Router (router-name [_] - :segment-router) + :trie-router) (routes [_] routes) (compiled-routes [_] @@ -345,7 +345,7 @@ ([compiled-routes opts] (let [{wild true, lookup false} (group-by impl/wild-route? compiled-routes) ->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router) - wildcard-router (segment-router wild opts) + wildcard-router (trie-router wild opts) static-router (->static-router lookup opts) names (find-names compiled-routes opts) routes (uncompile-routes compiled-routes)] @@ -446,7 +446,7 @@ (and (= 1 (count compiled-routes)) (not wilds?)) single-static-path-router path-conflicting quarantine-router (not wilds?) lookup-router - all-wilds? segment-router + all-wilds? trie-router :else mixed-router)] (when-let [validate (:validate opts)] diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 7ef8e481..aa4fb2fc 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -35,7 +35,9 @@ (concat ss (-static from to)) (condp = (get s to) \{ (let [to' (or (str/index-of s "}" to) (throw (ex-info (str "Unbalanced brackets: " (pr-str s)) {})))] - (recur (concat ss (-static from to) (-wild to to')) (inc to') (inc to'))) + (if (= \* (get s (inc to))) + (recur (concat ss (-static from to) (-catch-all (inc to) to')) (inc to') (inc to')) + (recur (concat ss (-static from to) (-wild to to')) (inc to') (inc to')))) \: (let [to' (or (str/index-of s "/" to) (count s))] (recur (concat ss (-static from to) (-wild to to')) to' to')) \* (let [to' (count s)] diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 12ceee64..d81e6c19 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -103,7 +103,7 @@ (is (= nil (matches "")))))) r/linear-router :linear-router - r/segment-router :segment-router + r/trie-router :trie-router r/mixed-router :mixed-router r/quarantine-router :quarantine-router)) @@ -142,7 +142,7 @@ r/lookup-router :lookup-router r/single-static-path-router :single-static-path-router r/linear-router :linear-router - r/segment-router :segment-router + r/trie-router :trie-router r/mixed-router :mixed-router r/quarantine-router :quarantine-router)) From 8628f0cec619f6c158b999d723a1329b988be6c1 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 16:45:57 +0200 Subject: [PATCH 08/51] Remove segment-rouiter code --- .../java-src/reitit/SegmentTrie.java | 319 ------------------ modules/reitit-core/src/reitit/segment.cljc | 75 ---- .../clj/reitit/prefix_tree_perf_test.clj | 16 +- test/cljc/reitit/segment_test.cljc | 26 -- test/cljc/reitit/trie_test.cljc | 26 ++ 5 files changed, 29 insertions(+), 433 deletions(-) delete mode 100644 modules/reitit-core/java-src/reitit/SegmentTrie.java delete mode 100644 modules/reitit-core/src/reitit/segment.cljc delete mode 100644 test/cljc/reitit/segment_test.cljc create mode 100644 test/cljc/reitit/trie_test.cljc diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java deleted file mode 100644 index d1020b24..00000000 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ /dev/null @@ -1,319 +0,0 @@ -package reitit; - -import clojure.lang.Keyword; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.*; - -public class SegmentTrie { - - 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; - } - } - if (start <= size) { - segments.add(path.substring(start, size)); - } - return segments; - } - - private static String decode(String s) { - try { - if (s.contains("%")) { - String _s = s; - if (s.contains("+")) { - _s = s.replace("+", "%2B"); - } - return URLDecoder.decode(_s, "UTF-8"); - } - } catch (UnsupportedEncodingException ignored) { - } - return s; - } - - public static class Match { - public final 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; - - public SegmentTrie add(String path, Object data) { - List paths = split(path); - SegmentTrie pointer = this; - for (String p : paths) { - if (p.startsWith(":")) { - Keyword k = Keyword.intern(p.substring(1)); - SegmentTrie s = pointer.wilds.get(k); - if (s == null) { - s = new SegmentTrie(); - pointer.wilds.put(k, s); - } - 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(); - pointer.catchAll.put(k, s); - } - pointer = s; - break; - } else { - SegmentTrie s = pointer.childs.get(p); - if (s == null) { - s = new SegmentTrie(); - pointer.childs.put(p, s); - } - pointer = s; - } - } - pointer.data = data; - 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); - 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()); - } 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); - } - } else if (!childs.isEmpty()) { - m = staticMatcher(); - if (data != null) { - m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m)); - } - } else { - return new DataMatcher(data); - } - 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) { - if (i < segments.size() && !segments.get(i).isEmpty()) { - final Match m = child.match(i + 1, segments, match); - if (m != null) { - m.params.put(parameter, decode(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) { - if (i < segments.size()) { - match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size())))); - match.data = data; - return match; - } - return null; - } - - @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) { - if (i < segments.size()) { - 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() : "nil"); - } - } - - public static Matcher scanner(List matchers) { - return new LinearMatcher(matchers); - } - - public static Match lookup(Matcher matcher, String path) { - return matcher.match(0, split(path), new Match()); - } - - public static void main(String[] args) { - - SegmentTrie trie = new SegmentTrie(); - 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, "/repos/metosin/reitit/stargazers")); - } -} diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc deleted file mode 100644 index a12f06f9..00000000 --- a/modules/reitit-core/src/reitit/segment.cljc +++ /dev/null @@ -1,75 +0,0 @@ -(ns reitit.segment - (:refer-clojure :exclude [-lookup compile]) - (:require [reitit.impl :as impl] - [clojure.string :as str]) - #?(:clj (:import (reitit SegmentTrie SegmentTrie$Match)))) - -(defrecord Match [data path-params]) - -(defprotocol Segment - (-insert [this ps data]) - (-lookup [this ps path-params])) - -(extend-protocol Segment - nil - (-insert [_ _ _]) - (-lookup [_ _ _])) - -(defn- -catch-all [children catch-all path-params p ps] - (-lookup - (impl/fast-get children catch-all) - nil - (assoc path-params catch-all (str/join "/" (cons p ps))))) - -(defn- segment - ([] (segment {} #{} nil nil)) - ([children wilds catch-all match] - (let [children' (impl/fast-map children) - wilds? (seq wilds)] - ^{:type ::segment} - (reify - Segment - (-insert [_ [p & ps] d] - (if-not p - (segment children wilds catch-all d) - (let [[w c] ((juxt impl/wild-param impl/catch-all-param) p) - wilds (if w (conj wilds w) wilds) - catch-all (or c catch-all) - children (update children (or w c p) #(-insert (or % (segment)) ps d))] - (segment children wilds catch-all match)))) - (-lookup [_ [p & ps] path-params] - (if (nil? p) - (when match (assoc match :path-params path-params)) - (or (-lookup (impl/fast-get children' p) ps path-params) - (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 - "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 [trie] - "Compiles the Trie so that [[lookup]] can be used." - #?(:cljs trie - :clj (.matcher (or ^SegmentTrie trie (SegmentTrie.))))) - -(defn scanner [compiled-tries] - "Returns a new compiled trie that does linear scan on the given compiled tries on [[lookup]]." - #?(:cljs (reify - Segment - (-lookup [_ ps params] - (some (fn [trie] (-lookup trie ps params)) compiled-tries))) - :clj (SegmentTrie/scanner compiled-tries))) - -(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 trie path)] - (->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match)))))) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index b56f2c58..aeed5516 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -1,10 +1,8 @@ (ns reitit.prefix-tree-perf-test (:require [clojure.test :refer :all] [io.pedestal.http.route.prefix-tree :as p] - [reitit.segment :as segment] [reitit.trie :as trie] - [criterium.core :as cc]) - (:import (reitit SegmentTrie))) + [criterium.core :as cc])) ;; ;; testing @@ -71,14 +69,6 @@ (p/insert acc p d)) nil routes)) -(def segment-matcher - (.matcher - ^SegmentTrie - (reduce - (fn [acc [p d]] - (segment/insert acc p d)) - nil routes))) - (def trie-matcher (trie/compile (reduce @@ -117,7 +107,7 @@ ;; 0.63µs (Single sweep path paraµs) ;; 0.51µs (Cleanup) ;; 0.30µs (Java) - (cc/quick-bench + #_(cc/quick-bench (segment/lookup segment-matcher "/v1/orgs/1/topics")) ;; 0.32µs (initial) @@ -134,5 +124,5 @@ (comment (p/lookup pedestal-tree "/v1/orgs/1/topics") (trie/lookup trie-matcher "/v1/orgs/1/topics") - (segment/lookup segment-matcher "/v1/orgs/1/topics")) + #_(segment/lookup segment-matcher "/v1/orgs/1/topics")) diff --git a/test/cljc/reitit/segment_test.cljc b/test/cljc/reitit/segment_test.cljc deleted file mode 100644 index 2be592e1..00000000 --- a/test/cljc/reitit/segment_test.cljc +++ /dev/null @@ -1,26 +0,0 @@ -(ns reitit.segment-test - (:require [clojure.test :refer [deftest testing is are]] - [reitit.segment :as s])) - -(deftest tests - (is (= (s/->Match {:a 1} {}) - (-> (s/insert nil "/foo" {:a 1}) - (s/compile) - (s/lookup "/foo")))) - - (is (= (s/->Match {:a 1} {}) - (-> (s/insert nil "/foo" {:a 1}) - (s/insert "/foo/*bar" {:b 1}) - (s/compile) - (s/lookup "/foo")))) - - (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")))) - - (is (= (s/->Match {:a 1} {}) - (-> (s/insert nil "" {:a 1}) - (s/compile) - (s/lookup ""))))) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc new file mode 100644 index 00000000..570c45c5 --- /dev/null +++ b/test/cljc/reitit/trie_test.cljc @@ -0,0 +1,26 @@ +(ns reitit.trie-test + (:require [clojure.test :refer [deftest testing is are]] + [reitit.trie :as rt])) + +(deftest tests + (is (= (rt/->Match {:a 1} {}) + (-> (rt/insert nil "/foo" {:a 1}) + (rt/compile) + (rt/lookup "/foo")))) + + (is (= (rt/->Match {:a 1} {}) + (-> (rt/insert nil "/foo" {:a 1}) + (rt/insert "/foo/*bar" {:b 1}) + (rt/compile) + (rt/lookup "/foo")))) + + (is (= (rt/->Match {:b 1} {:bar "bar"}) + (-> (rt/insert nil "/foo" {:a 1}) + (rt/insert "/foo/*bar" {:b 1}) + (rt/compile) + (rt/lookup "/foo/bar")))) + + (is (= (rt/->Match {:a 1} {}) + (-> (rt/insert nil "" {:a 1}) + (rt/compile) + (rt/lookup ""))))) From 4c0d2fb285584c19f24cb7b368f8d1ae6243f49e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 16:50:44 +0200 Subject: [PATCH 09/51] mega-insert for trie! --- modules/reitit-core/src/reitit/trie.cljc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index aa4fb2fc..a1f3aa9c 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -89,8 +89,16 @@ (update :children dissoc "")) node'))) -(defn insert [node path data] - (-insert (or node (-node {})) (-split path) data)) +(defn insert + ([routes] + (insert nil routes)) + ([node routes] + (reduce + (fn [acc [p d]] + (insert acc p d)) + node routes)) + ([node path data] + (-insert (or node (-node {})) (-split path) data))) (defn ^Trie$Matcher compile [{:keys [data children wilds catch-all]}] (let [matchers (cond-> [] From 42d6d0c78ddbac0a9b089e08162caef7025c95d0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 21:25:33 +0200 Subject: [PATCH 10/51] Priorize trie based on depth --- modules/reitit-core/java-src/reitit/Trie.java | 28 +++++++++++++++++++ modules/reitit-core/src/reitit/trie.cljc | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 55250fdc..b9b3b167 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -67,6 +67,8 @@ public class Trie { public interface Matcher { Match match(int i, Path path, Match match); + + int depth(); } public static StaticMatcher staticMatcher(String path, Matcher child) { @@ -98,6 +100,11 @@ public class Trie { return child.match(i + size, path, match); } + @Override + public int depth() { + return child.depth() + 1; + } + @Override public String toString() { return "[\"" + new String(path) + "\" " + child + "]"; @@ -124,6 +131,11 @@ public class Trie { return null; } + @Override + public int depth() { + return 1; + } + @Override public String toString() { return (data != null ? data.toString() : "nil"); @@ -171,6 +183,11 @@ public class Trie { return null; } + @Override + public int depth() { + return child.depth() + 1; + } + @Override public String toString() { return "[" + key + " " + child + "]"; @@ -200,6 +217,11 @@ public class Trie { return null; } + @Override + public int depth() { + return 1; + } + @Override public String toString() { return "[" + parameter + " " + new DataMatcher(data) + "]"; @@ -217,6 +239,7 @@ public class Trie { LinearMatcher(List childs) { this.childs = childs.toArray(new Matcher[0]); + Arrays.sort(this.childs, Comparator.comparing(Matcher::depth).reversed()); this.size = childs.size(); } @@ -231,6 +254,11 @@ public class Trie { return null; } + @Override + public int depth() { + return Arrays.stream(childs).mapToInt(Matcher::depth).max().orElseThrow(NoSuchElementException::new); + } + @Override public String toString() { return Arrays.toString(childs); diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index a1f3aa9c..a6417423 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -85,7 +85,7 @@ (assoc-in node [:children path] (-insert (-node {}) ps data))))] (if-let [child (get-in node' [:children ""])] ;; optimize by removing empty paths - (-> (merge-with merge node' child) + (-> (merge-with merge (dissoc node' :data) child) (update :children dissoc "")) node'))) From 8755e19f78449d5a2d8114cf5a66d1d8d02cbec2 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 29 Jan 2019 21:59:17 +0200 Subject: [PATCH 11/51] Cleanup --- modules/reitit-core/src/reitit/trie.cljc | 91 ++++++++++++++++-------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index a6417423..40384fc2 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -91,7 +91,7 @@ (defn insert ([routes] - (insert nil routes)) + (insert nil routes)) ([node routes] (reduce (fn [acc [p d]] @@ -110,10 +110,8 @@ (Trie/linearMatcher matchers) (first matchers)))) -(defn pretty [{:keys [data children wilds catch-all]}] - (into - (if data [data] []) - (mapcat (fn [[p n]] [p (pretty n)]) (concat children wilds catch-all)))) +(defn pretty [matcher] + (-> matcher str read-string eval)) (defn lookup [^Trie$Matcher matcher path] (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] @@ -122,22 +120,67 @@ (defn scanner [compiled-tries] (Trie/scanner compiled-tries)) -;; -;; matcher -;; - ;; ;; spike ;; -(-> nil - (insert "/:abba" 1) - (insert "/kikka" 2) - (insert "/kikka/kakka/kukka" 3) - (insert "/kikka/:kakka/kukka" 4) - (insert "/kikka/kuri/{user/doc}.html" 5) - (insert "/kikkare/*path" 6) - #_(pretty)) +(-> + [["/v2/whoami" 1] + ["/v2/users/:user-id/datasets" 2] + ["/v2/public/projects/:project-id/datasets" 3] + ["/v1/public/topics/:topic" 4] + ["/v1/users/:user-id/orgs/:org-id" 5] + ["/v1/search/topics/:term" 6] + ["/v1/users/:user-id/invitations" 7] + ["/v1/users/:user-id/topics" 9] + ["/v1/users/:user-id/bookmarks/followers" 10] + ["/v2/datasets/:dataset-id" 11] + ["/v1/orgs/:org-id/usage-stats" 12] + ["/v1/orgs/:org-id/devices/:client-id" 13] + ["/v1/messages/user/:user-id" 14] + ["/v1/users/:user-id/devices" 15] + ["/v1/public/users/:user-id" 16] + ["/v1/orgs/:org-id/errors" 17] + ["/v1/public/orgs/:org-id" 18] + ["/v1/orgs/:org-id/invitations" 19] + ["/v1/users/:user-id/device-errors" 22] + ["/v2/login" 23] + ["/v1/users/:user-id/usage-stats" 24] + ["/v2/users/:user-id/devices" 25] + ["/v1/users/:user-id/claim-device/:client-id" 26] + ["/v2/public/projects/:project-id" 27] + ["/v2/public/datasets/:dataset-id" 28] + ["/v2/users/:user-id/topics/bulk" 29] + ["/v1/messages/device/:client-id" 30] + ["/v1/users/:user-id/owned-orgs" 31] + ["/v1/topics/:topic" 32] + ["/v1/users/:user-id/bookmark/:topic" 33] + ["/v1/orgs/:org-id/members/:user-id" 34] + ["/v1/users/:user-id/devices/:client-id" 35] + ["/v1/users/:user-id" 36] + ["/v1/orgs/:org-id/devices" 37] + ["/v1/orgs/:org-id/members" 38] + ["/v2/orgs/:org-id/topics" 40] + ["/v1/whoami" 41] + ["/v1/orgs/:org-id" 42] + ["/v1/users/:user-id/api-key" 43] + ["/v2/schemas" 44] + ["/v2/users/:user-id/topics" 45] + ["/v1/orgs/:org-id/confirm-membership/:token" 46] + ["/v2/topics/:topic" 47] + ["/v1/messages/topic/:topic" 48] + ["/v1/users/:user-id/devices/:client-id/reset-password" 49] + ["/v2/topics" 50] + ["/v1/login" 51] + ["/v1/users/:user-id/orgs" 52] + ["/v2/public/messages/dataset/:dataset-id" 53] + ["/v1/topics" 54] + ["/v1/orgs" 55] + ["/v1/users/:user-id/bookmarks" 56] + ["/v1/orgs/:org-id/topics" 57]] + (insert) + (compile) + (pretty)) (-> nil (insert "/kikka" 2) @@ -145,16 +188,4 @@ (insert "/kikka/:kakka/kurkku" 4) (insert "/kikka/kuri/{user/doc}/html" 5) (compile) - (lookup "/kikka/kakka/kurkku")) - -;; => - -["/" - ["kikka" [2 - "/" ["k" ["akka/kukka" [3] - "uri/" [:user/doc [".html" [5]]]] - :kakka ["/kukka" [4]]] - "re/" [#{:path} [6]]] - :abba [1]]] - - + (pretty)) From 6f902d118ae7315a9b2077dff7b7a6885f175e3d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 17:15:59 +0200 Subject: [PATCH 12/51] Free from the regex! --- modules/reitit-core/java-src/reitit/Trie.java | 1 - modules/reitit-core/src/reitit/core.cljc | 147 ++++-------- modules/reitit-core/src/reitit/impl.cljc | 216 ++++++++++-------- modules/reitit-core/src/reitit/trie.cljc | 50 ++-- .../reitit-swagger/src/reitit/swagger.cljc | 10 +- perf-test/clj/reitit/impl_perf_test.clj | 9 - .../clj/reitit/opensensors_perf_test.clj | 12 +- project.clj | 5 +- test/cljc/reitit/core_test.cljc | 32 ++- test/cljc/reitit/impl_test.cljc | 36 ++- test/cljc/reitit/swagger_test.clj | 60 ++--- 11 files changed, 303 insertions(+), 275 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index b9b3b167..ac88937a 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -34,7 +34,6 @@ public class Trie { hasPlus = true; } } - System.err.println(); return decode(chars, offset, count, hasPercent, hasPlus); } diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 307ba91e..7c2c8919 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,10 +1,11 @@ (ns reitit.core - (:require [meta-merge.core :refer [meta-merge]] - [clojure.string :as str] - [reitit.trie :as trie] - [reitit.impl :as impl #?@(:cljs [:refer [Route]])]) - #?(:clj - (:import (reitit.impl Route)))) + (:require [clojure.string :as str] + [reitit.impl :as impl] + [reitit.trie :as trie])) + +;; +;; Expand +;; (defprotocol Expand (expand [this opts])) @@ -30,56 +31,9 @@ nil (expand [_ _])) -(defn walk [raw-routes {:keys [path data routes expand] - :or {data [], routes [], expand expand} - :as opts}] - (letfn - [(walk-many [p m r] - (reduce #(into %1 (walk-one p m %2)) [] r)) - (walk-one [pacc macc routes] - (if (vector? (first routes)) - (walk-many pacc macc routes) - (when (string? (first routes)) - (let [[path & [maybe-arg :as args]] routes - [data childs] (if (or (vector? maybe-arg) - (and (sequential? maybe-arg) - (sequential? (first maybe-arg))) - (nil? maybe-arg)) - [{} args] - [maybe-arg (rest args)]) - macc (into macc (expand data opts)) - child-routes (walk-many (str pacc path) macc (keep identity childs))] - (if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))] - (walk-one path (mapv identity data) raw-routes))) - -(defn map-data [f routes] - (mapv #(update % 1 f) routes)) - -(defn merge-data [x] - (reduce - (fn [acc [k v]] - (meta-merge acc {k v})) - {} x)) - -(defn resolve-routes [raw-routes {:keys [coerce] :as opts}] - (cond->> (->> (walk raw-routes opts) (map-data merge-data)) - coerce (into [] (keep #(coerce % opts))))) - -(defn path-conflicting-routes [routes] - (-> (into {} - (comp (map-indexed (fn [index route] - [route (into #{} - (filter #(impl/conflicting-routes? route %)) - (subvec routes (inc index)))])) - (filter (comp seq second))) - routes) - (not-empty))) - -(defn conflicting-paths [conflicts] - (->> (for [[p pc] conflicts] - (conj (map first pc) (first p))) - (apply concat) - (set))) +;; +;; Conflicts +;; (defn path-conflicts-str [conflicts] (apply str "Router contains conflicting route paths:\n\n" @@ -88,15 +42,6 @@ (str " " path "\n-> " (str/join "\n-> " (mapv first vals)) "\n\n")) conflicts))) -(defn name-conflicting-routes [routes] - (some->> routes - (group-by (comp :name second)) - (remove (comp nil? first)) - (filter (comp pos? count butlast second)) - (seq) - (map (fn [[k v]] [k (set v)])) - (into {}))) - (defn name-conflicts-str [conflicts] (apply str "Router contains conflicting route names:\n\n" (mapv @@ -110,23 +55,9 @@ (f conflicts) {:conflicts conflicts}))) -(defn- name-lookup [[_ {:keys [name]}] _] - (if name #{name})) - -(defn- find-names [routes _] - (into [] (keep #(-> % second :name)) routes)) - -(defn- compile-route [[p m :as route] {:keys [compile] :as opts}] - [p m (if compile (compile route opts))]) - -(defn- compile-routes [routes opts] - (into [] (keep #(compile-route % opts) routes))) - -(defn- uncompile-routes [routes] - (mapv (comp vec (partial take 2)) routes)) - -(defn route-info [route] - (impl/create route)) +;; +;; Router +;; (defprotocol Router (router-name [this]) @@ -162,26 +93,30 @@ ([match query-params] (some-> match :path (cond-> query-params (str "?" (impl/query-string query-params)))))) +;; +;; Different routers +;; + (defn linear-router "Creates a linear-router from resolved routes and optional expanded options. See [[router]] for available options." ([compiled-routes] (linear-router compiled-routes {})) ([compiled-routes opts] - (let [names (find-names compiled-routes opts) + (let [names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] - (let [{:keys [path-params] :as route} (impl/create [p data result]) + (let [{:keys [path-params] :as route} (impl/parse p) f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) - (->PartialMatch p data result % path-params))] + (->PartialMatch p data result (impl/url-decode-coll %) path-params))] [(conj pl (-> (trie/insert nil p (->Match p data result nil nil)) (trie/compile))) (if name (assoc nl name f) nl)])) [[] {}] compiled-routes) lookup (impl/fast-map nl) scanner (trie/scanner pl) - routes (uncompile-routes compiled-routes)] + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router @@ -219,7 +154,7 @@ (str "can't create :lookup-router with wildcard routes: " wilds) {:wilds wilds :routes compiled-routes}))) - (let [names (find-names compiled-routes opts) + (let [names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] [(assoc pl p (->Match p data result {} p)) @@ -230,7 +165,7 @@ compiled-routes) data (impl/fast-map pl) lookup (impl/fast-map nl) - routes (uncompile-routes compiled-routes)] + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router (router-name [_] @@ -258,20 +193,20 @@ ([compiled-routes] (trie-router compiled-routes {})) ([compiled-routes opts] - (let [names (find-names compiled-routes opts) + (let [names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] - (let [{:keys [path-params] :as route} (impl/create [p data result]) + (let [{:keys [path-params] :as route} (impl/parse p) f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) - (->PartialMatch p data result % path-params))] + (->PartialMatch p data result (impl/url-decode-coll %) path-params))] [(trie/insert pl p (->Match p data result nil nil)) (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) pl (trie/compile pl) lookup (impl/fast-map nl) - routes (uncompile-routes compiled-routes)] + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router @@ -308,11 +243,11 @@ (ex-info (str ":single-static-path-router requires exactly 1 static route: " compiled-routes) {:routes compiled-routes}))) - (let [[n :as names] (find-names compiled-routes opts) + (let [[n :as names] (impl/find-names compiled-routes opts) [[p data result]] compiled-routes p #?(:clj (.intern ^String p) :cljs p) match (->Match p data result {} p) - routes (uncompile-routes compiled-routes)] + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router (router-name [_] @@ -347,8 +282,8 @@ ->static-router (if (= 1 (count lookup)) single-static-path-router lookup-router) wildcard-router (trie-router wild opts) static-router (->static-router lookup opts) - names (find-names compiled-routes opts) - routes (uncompile-routes compiled-routes)] + names (impl/find-names compiled-routes opts) + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router (router-name [_] @@ -378,13 +313,13 @@ ([compiled-routes] (quarantine-router compiled-routes {})) ([compiled-routes opts] - (let [conflicting-paths (-> compiled-routes path-conflicting-routes conflicting-paths) + (let [conflicting-paths (-> compiled-routes impl/path-conflicting-routes impl/conflicting-paths) conflicting? #(contains? conflicting-paths (first %)) {conflicting true, non-conflicting false} (group-by conflicting? compiled-routes) linear-router (linear-router conflicting opts) mixed-router (mixed-router non-conflicting opts) - names (find-names compiled-routes opts) - routes (uncompile-routes compiled-routes)] + names (impl/find-names compiled-routes opts) + routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify Router (router-name [_] @@ -407,8 +342,12 @@ (or (match-by-name mixed-router name path-params) (match-by-name linear-router name path-params))))))) +;; +;; Creating Routers +;; + (defn ^:no-doc default-router-options [] - {:lookup name-lookup + {:lookup (fn [[_ {:keys [name]}] _] (if name #{name})) :expand expand :coerce (fn [route _] route) :compile (fn [[_ {:keys [handler]}] _] handler) @@ -435,10 +374,10 @@ (router raw-routes {})) ([raw-routes opts] (let [{:keys [router] :as opts} (merge (default-router-options) opts) - routes (resolve-routes raw-routes opts) - path-conflicting (path-conflicting-routes routes) - name-conflicting (name-conflicting-routes routes) - compiled-routes (compile-routes routes opts) + routes (impl/resolve-routes raw-routes opts) + path-conflicting (impl/path-conflicting-routes routes) + name-conflicting (impl/name-conflicting-routes routes) + compiled-routes (impl/compile-routes routes opts) wilds? (boolean (some impl/wild-route? compiled-routes)) all-wilds? (every? impl/wild-route? compiled-routes) router (cond diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 540e7148..aa33e705 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -1,12 +1,29 @@ (ns ^:no-doc reitit.impl #?(:cljs (:require-macros [reitit.impl])) (:require [clojure.string :as str] - [clojure.set :as set]) + [clojure.set :as set] + [meta-merge.core :as mm] + [reitit.trie :as trie]) #?(:clj (:import (java.util.regex Pattern) (java.util HashMap Map) - (java.net URLEncoder URLDecoder) - (reitit SegmentTrie)))) + (java.net URLEncoder URLDecoder)))) + +(defn normalize [s] + (-> s (trie/split-path) (trie/join-path))) + +(defrecord Route [path path-parts path-params]) + +(defn parse [path] + (let [path #?(:clj (.intern ^String (normalize path)) :cljs (normalize path)) + path-parts (trie/split-path path) + path-params (->> path-parts (remove string?) (map :value) set)] + (map->Route {:path-params path-params + :path-parts path-parts + :path path}))) + +(defn wild-route? [[path]] + (-> path parse :path-params seq boolean)) (defn maybe-map-values "Applies a function to every value of a map, updates the value if not nil. @@ -20,107 +37,128 @@ coll coll)) -(defn segments - "Splits the path into sequence of segments, using `/` char. Assumes that the - path starts with `/`, stripping the first empty segment. e.g. +(defn- -slice-start [[p1 :as p1s] [p2 :as p2s]] + (let [-split (fn [p] + (if-let [i (and p (str/index-of p "/"))] + [(subs p 0 i) (subs p i)] + [p])) + -slash (fn [cp p] + (cond + (not (string? cp)) [cp] + (and (string? cp) (not= (count cp) (count p))) [(subs p (count cp))] + (and (string? p) (not cp)) (-split p))) + -postcut (fn [[p :as pps]] + (let [i (and p (str/index-of p "/"))] + (if (and i (pos? i)) + (concat [(subs p 0 i) (subs p i)] (rest pps)) + pps))) + -tailcut (fn [cp [p :as ps]] (concat (-slash cp p) (rest ps)))] + (if (or (nil? p1) (nil? p2)) + [(-postcut p1s) (-postcut p2s)] + (let [cp (and (string? p1) (string? p2) (trie/common-prefix p1 p2))] + [(-tailcut cp p1s) (-tailcut cp p2s)])))) - (segments \"/a/b/c\") ; => (\"a\" \"b\" \"c\") - (segments \"/a/) ; => (\"a\" \"\")" - [path] - #?(:clj (SegmentTrie/split ^String path) - :cljs (rest (.split path #"/" 666)))) +(defn- -slice-end [x xs] + (let [i (if (string? x) (str/index-of x "/"))] + (if (and (number? i) (pos? i)) + (concat [(subs x i)] xs) + xs))) -;; -;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj -;; +(defn conflicting-routes? [route1 route2] + (loop [parts1 (-> route1 first parse :path-parts) + parts2 (-> route2 first parse :path-parts)] + (let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)] + (cond + (= s1 s2 nil) true + (or (nil? s1) (nil? s2)) false + (or (trie/catch-all? s1) (trie/catch-all? s2)) true + (or (trie/wild? s1) (trie/wild? s2)) (recur (-slice-end s1 ss1) (-slice-end s2 ss2)) + (not= s1 s2) false + :else (recur ss1 ss2))))) -(defn wild? [s] - (contains? #{\: \*} (first (str s)))) +(defn walk [raw-routes {:keys [path data routes expand] + :or {data [], routes []} + :as opts}] + (letfn + [(walk-many [p m r] + (reduce #(into %1 (walk-one p m %2)) [] r)) + (walk-one [pacc macc routes] + (if (vector? (first routes)) + (walk-many pacc macc routes) + (when (string? (first routes)) + (let [[path & [maybe-arg :as args]] routes + [data childs] (if (or (vector? maybe-arg) + (and (sequential? maybe-arg) + (sequential? (first maybe-arg))) + (nil? maybe-arg)) + [{} args] + [maybe-arg (rest args)]) + macc (into macc (expand data opts)) + child-routes (walk-many (str pacc path) macc (keep identity childs))] + (if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))] + (walk-one path (mapv identity data) raw-routes))) -(defn catch-all? [s] - (= \* (first (str s)))) +(defn map-data [f routes] + (mapv #(update % 1 f) routes)) -(defn wild-param [s] - (let [ss (str s)] - (if (= \: (first ss)) - (keyword (subs ss 1))))) +(defn merge-data [x] + (reduce + (fn [acc [k v]] + (mm/meta-merge acc {k v})) + {} x)) -(defn catch-all-param [s] - (let [ss (str s)] - (if (= \* (first ss)) - (keyword (subs ss 1))))) +(defn resolve-routes [raw-routes {:keys [coerce] :as opts}] + (cond->> (->> (walk raw-routes opts) (map-data merge-data)) + coerce (into [] (keep #(coerce % opts))))) -(defn wild-or-catch-all-param? [x] - (boolean (or (wild-param x) (catch-all-param x)))) +(defn path-conflicting-routes [routes] + (-> (into {} + (comp (map-indexed (fn [index route] + [route (into #{} + (filter #(conflicting-routes? route %)) + (subvec routes (inc index)))])) + (filter (comp seq second))) + routes) + (not-empty))) -(defn contains-wilds? [path] - (boolean (some wild-or-catch-all-param? (segments path)))) +(defn conflicting-paths [conflicts] + (->> (for [[p pc] conflicts] + (conj (map first pc) (first p))) + (apply concat) + (set))) -;; -;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/path.clj -;; +(defn name-conflicting-routes [routes] + (some->> routes + (group-by (comp :name second)) + (remove (comp nil? first)) + (filter (comp pos? count butlast second)) + (seq) + (map (fn [[k v]] [k (set v)])) + (into {}))) -(defn- parse-path-token [out string] - (condp re-matches string - #"^:(.+)$" :>> (fn [[_ token]] - (let [key (keyword token)] - (-> out - (update-in [:path-parts] conj key) - (update-in [:path-params] conj key)))) - #"^\*(.*)$" :>> (fn [[_ token]] - (let [key (keyword token)] - (-> out - (update-in [:path-parts] conj key) - (update-in [:path-params] conj key)))) - (update-in out [:path-parts] conj string))) +(defn find-names [routes _] + (into [] (keep #(-> % second :name)) routes)) -(defn- parse-path - ([pattern] (parse-path {:path-parts [] :path-params #{}} pattern)) - ([accumulated-info pattern] - (if-let [m (re-matches #"/(.*)" pattern)] - (let [[_ path] m] - (reduce parse-path-token - accumulated-info - (str/split path #"/"))) - (throw (ex-info "Routes must start from the root, so they must begin with a '/'" {:pattern pattern}))))) +(defn compile-route [[p m :as route] {:keys [compile] :as opts}] + [p m (if compile (compile route opts))]) -;; -;; Routing (c) Metosin -;; +(defn compile-routes [routes opts] + (into [] (keep #(compile-route % opts) routes))) -(defrecord Route [path path-parts path-params data result]) - -(defn create [[path data result]] - (let [path #?(:clj (.intern ^String path) :cljs path) - {:keys [path-parts path-params]} (parse-path path)] - (map->Route - {:path-params path-params - :path-parts path-parts - :path path - :result result - :data data}))) - -(defn wild-route? [[path]] - (contains-wilds? path)) - -(defn conflicting-routes? [[p1] [p2]] - (loop [[s1 & ss1] (segments p1) - [s2 & ss2] (segments p2)] - (cond - (= s1 s2 nil) true - (or (nil? s1) (nil? s2)) false - (or (catch-all? s1) (catch-all? s2)) true - (or (wild? s1) (wild? s2)) (recur ss1 ss2) - (not= s1 s2) false - :else (recur ss1 ss2)))) +(defn uncompile-routes [routes] + (mapv (comp vec (partial take 2)) routes)) (defn path-for [^Route route path-params] - (if-let [required (:path-params route)] - (if (every? #(contains? path-params %) required) - (->> (:path-parts route) - (map #(get (or path-params {}) % %)) - (str/join \/) - (str "/"))) + (if (:path-params route) + (if-let [parts (reduce + (fn [acc part] + (if (string? part) + (conj acc part) + (if-let [p (get path-params (:value part))] + (conj acc p) + (reduced nil)))) + [] (:path-parts route))] + (apply str parts)) (:path route))) (defn throw-on-missing-path-params [template required path-params] diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 40384fc2..46b0a3a6 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -3,11 +3,16 @@ (:require [clojure.string :as str]) (:import [reitit Trie Trie$Match Trie$Matcher])) +(defrecord Wild [value]) +(defrecord CatchAll [value]) (defrecord Match [data path-params]) (defrecord Node [children wilds catch-all data]) +(defn wild? [x] (instance? Wild x)) +(defn catch-all? [x] (instance? CatchAll x)) + ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix -(defn- -common-prefix [s1 s2] +(defn common-prefix [s1 s2] (let [max (min (count s1) (count s2))] (loop [i 0] (cond @@ -26,10 +31,10 @@ (keyword (subs s 0 i) (subs s (inc i))) (keyword s))) -(defn- -split [s] +(defn split-path [s] (let [-static (fn [from to] (if-not (= from to) [(subs s from to)])) - -wild (fn [from to] [(-keyword (subs s (inc from) to))]) - -catch-all (fn [from to] [#{(keyword (subs s (inc from) to))}])] + -wild (fn [from to] [(->Wild (-keyword (subs s (inc from) to)))]) + -catch-all (fn [from to] [(->CatchAll (keyword (subs s (inc from) to)))])] (loop [ss nil, from 0, to 0] (if (= to (count s)) (concat ss (-static from to)) @@ -44,6 +49,15 @@ (recur (concat ss (-static from to) (-catch-all to to')) to' to')) (recur ss from (inc to))))))) +(defn join-path [xs] + (reduce + (fn [s x] + (str s (cond + (string? x) x + (instance? Wild x) (str "{" (-> x :value str (subs 1)) "}") + (instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}")))) + "" xs)) + (defn- -node [m] (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) @@ -53,11 +67,11 @@ (nil? path) (assoc node :data data) - (keyword? path) - (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))) + (instance? Wild path) + (update-in node [:wilds (:value path)] (fn [n] (-insert (or n (-node {})) ps data))) - (set? path) - (assoc-in node [:catch-all path] (-node {:data data})) + (instance? CatchAll path) + (assoc-in node [:catch-all (:value path)] (-node {:data data})) (str/blank? path) (-insert node ps data) @@ -66,7 +80,7 @@ (or (reduce (fn [_ [p n]] - (if-let [cp (-common-prefix p path)] + (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)] @@ -89,6 +103,10 @@ (update :children dissoc "")) node'))) +;; +;; public api +;; + (defn insert ([routes] (insert nil routes)) @@ -98,14 +116,14 @@ (insert acc p d)) node routes)) ([node path data] - (-insert (or node (-node {})) (-split path) data))) + (-insert (or node (-node {})) (split-path path) data))) (defn ^Trie$Matcher compile [{:keys [data children wilds catch-all]}] (let [matchers (cond-> [] data (conj (Trie/dataMatcher data)) children (into (for [[p c] children] (Trie/staticMatcher p (compile c)))) wilds (into (for [[p c] wilds] (Trie/wildMatcher p (compile c)))) - catch-all (into (for [[p c] catch-all] (Trie/catchAllMatcher (first p) (:data c)))))] + catch-all (into (for [[p c] catch-all] (Trie/catchAllMatcher p (:data c)))))] (if (rest matchers) (Trie/linearMatcher matchers) (first matchers)))) @@ -182,10 +200,10 @@ (compile) (pretty)) -(-> nil - (insert "/kikka" 2) - (insert "/kikka/kakka/kukka" 3) - (insert "/kikka/:kakka/kurkku" 4) - (insert "/kikka/kuri/{user/doc}/html" 5) +(-> [["/kikka" 2] + ["/kikka/kakka/kukka" 3] + ["/kikka/:kakka/kurkku" 4] + ["/kikka/kuri/{user/doc}/html" 5]] + (insert) (compile) (pretty)) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index b1551fdb..acd70fd7 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -64,12 +64,8 @@ {:name ::swagger :spec ::spec}) -(defn- path->template [path] - (->> (impl/segments path) - (map #(if (impl/wild-or-catch-all-param? %) - (str "{" (subs % 1) "}") %)) - (str/join "/") - (str "/"))) +(defn- swagger-path [path] + (-> path impl/normalize (str/replace #"\{\*" "{"))) (defn create-swagger-handler [] "Create a ring handler to emit swagger spec. Collects all routes from router which have @@ -100,7 +96,7 @@ (strip-top-level-keys swagger))])) transform-path (fn [[p _ c]] (if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))] - [(path->template p) endpoint]))] + [(swagger-path p) endpoint]))] (let [paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) (into {}))] {:status 200 :body (meta-merge swagger {:paths paths})}))) diff --git a/perf-test/clj/reitit/impl_perf_test.clj b/perf-test/clj/reitit/impl_perf_test.clj index faae130a..1f1f90ed 100644 --- a/perf-test/clj/reitit/impl_perf_test.clj +++ b/perf-test/clj/reitit/impl_perf_test.clj @@ -185,15 +185,6 @@ :c "1+1" :d "1"})) -(defn split! [] - - (suite "split") - - ;; 114ns (String/split) - ;; 82ns (SegmentTrie/split) - (test "Splitting a String") - (test! impl/segments "/olipa/kerran/:avaruus")) - (comment (url-decode!) (url-encode!) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 1f7465f7..60b69203 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -15,7 +15,8 @@ [io.pedestal.http.route.map-tree :as map-tree] [io.pedestal.http.route.router :as pedestal] [reitit.core :as r] - [criterium.core :as cc])) + [criterium.core :as cc] + [reitit.trie :as trie])) ;; ;; start repl with `lein perf repl` @@ -581,11 +582,11 @@ ;; 735ns (maybe-map-values) ;; 474ns (java-segment-router) ;; 373ms (trie) - (b! "reitit-ring" reitit-ring-f) + #_(b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) - (b! "reitit-ring-fast" reitit-ring-fast-f) + #_(b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) @@ -615,6 +616,11 @@ (comment (bench-rest!)) +(-> opensensors-routes + trie/insert + trie/compile + trie/pretty) + (set! *warn-on-reflection* true) (require '[clj-async-profiler.core :as prof]) diff --git a/project.clj b/project.clj index 0b3fa534..e37c2f8d 100644 --- a/project.clj +++ b/project.clj @@ -68,7 +68,10 @@ [org.clojure/clojurescript "1.10.439"] ;; modules dependencies - [metosin/reitit "0.2.13"] + ;[metosin/reitit "0.2.13"] + [meta-merge] + [metosin/schema-tools] + [metosin/spec-tools] [expound "0.7.2"] [orchestra "2018.12.06-2"] diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index d81e6c19..a845f6d3 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -1,6 +1,7 @@ (ns reitit.core-test (:require [clojure.test :refer [deftest testing is are]] - [reitit.core :as r #?@(:cljs [:refer [Match Router]])]) + [reitit.core :as r #?@(:cljs [:refer [Match Router]])] + [reitit.impl :as impl]) #?(:clj (:import (reitit.core Match Router) (clojure.lang ExceptionInfo)))) @@ -136,8 +137,9 @@ ExceptionInfo #"can't create :lookup-router with wildcard routes" (r/lookup-router - (r/resolve-routes - ["/api/:version/ping"] {})))))) + (impl/resolve-routes + ["/api/:version/ping"] + (r/default-router-options))))))) r/lookup-router :lookup-router r/single-static-path-router :single-static-path-router @@ -208,7 +210,7 @@ expected [["/auth/login" {:name :auth/login}] ["/auth/recovery/token/:token" {:name :auth/recovery}] ["/workspace/:project-uuid/:page-uuid" {:name :workspace/page}]]] - (is (= expected (r/resolve-routes routes {}))))) + (is (= expected (impl/resolve-routes routes (r/default-router-options)))))) (testing "ring sample" (let [pong (constantly "ok") @@ -226,7 +228,7 @@ ["/api/admin/user" {:mw [:api :admin], :roles #{:user}}] ["/api/admin/db" {:mw [:api :admin :db], :roles #{:admin}}]] router (r/router routes)] - (is (= expected (r/resolve-routes routes {}))) + (is (= expected (impl/resolve-routes routes (r/default-router-options)))) (is (= (r/map->Match {:template "/api/user/:id/:sub-id" :data {:mw [:api], :parameters {:id "String", :sub-id "String"}} @@ -237,10 +239,10 @@ (deftest conflicting-routes-test (testing "path conflicts" (are [conflicting? data] - (let [routes (r/resolve-routes data {}) + (let [routes (impl/resolve-routes data (r/default-router-options)) conflicts (-> routes - (r/resolve-routes {}) - (r/path-conflicting-routes))] + (impl/resolve-routes (r/default-router-options)) + (impl/path-conflicting-routes))] (if conflicting? (seq conflicts) (nil? conflicts))) true [["/a"] @@ -275,8 +277,8 @@ ["/:b" {}] #{["/c" {}] ["/*d" {}]}, ["/c" {}] #{["/*d" {}]}} (-> [["/a"] ["/:b"] ["/c"] ["/*d"]] - (r/resolve-routes {}) - (r/path-conflicting-routes))))) + (impl/resolve-routes (r/default-router-options)) + (impl/path-conflicting-routes))))) (testing "router with conflicting routes" (testing "throws by default" @@ -331,3 +333,13 @@ (let [router (r/router ["/endpoint" (->Named :kikka)])] (is (= [["/endpoint" {:name :kikka}]] (r/routes router))))) + +(r/router + [["/:abba" ::abba] + ["/abba/1" ::abba2] + ["/:jabba/2" ::jabba2] + ["/:abba/:dabba/doo" ::doo] + ["/abba/dabba/boo/baa" ::baa] + ["/abba/:dabba/boo" ::boo] + ["/:jabba/:dabba/:doo/:daa/*foo" ::wild]] + {:router r/trie-router}) diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index 2ac3cc4b..5ad83786 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -2,11 +2,37 @@ (:require [clojure.test :refer [deftest testing is are]] [reitit.impl :as impl])) -(deftest segments-test - (is (= ["api" "ipa" "beer" "craft" "bisse"] - (into [] (impl/segments "/api/ipa/beer/craft/bisse")))) - (is (= ["a" "" "b" "" "c" ""] - (into [] (impl/segments "/a//b//c/"))))) +(deftest normalize-test + (are [path expected] + (is (= expected (impl/normalize path))) + + "/olipa/:kerran/avaruus", "/olipa/{kerran}/avaruus" + "/olipa/{kerran}/avaruus", "/olipa/{kerran}/avaruus" + "/olipa/{a.b/c}/avaruus", "/olipa/{a.b/c}/avaruus" + "/olipa/kerran/*avaruus", "/olipa/kerran/{*avaruus}" + "/olipa/kerran/{*avaruus}", "/olipa/kerran/{*avaruus}" + "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) + +(deftest conflicting-route-test + (are [c? p1 p2] + (is (= c? (impl/conflicting-routes? [p1] [p2]))) + + true "/a" "/a" + true "/a" "/:a" + true "/a/:b" "/:a/b" + true "/ab/:b" "/:a/ba" + true "/*a" "/:a/ba/ca" + + true "/a" "/{a}" + true "/a/{b}" "/{a}/b" + true "/ab/{b}" "/{a}/ba" + true "/{*a}" "/{a}/ba/ca" + + false "/a" "/:a/b" + false "/a" "/:a/b" + + false "/a" "/{a}/b" + false "/a" "/{a}/b")) (deftest strip-nils-test (is (= {:a 1, :c false} (impl/strip-nils {:a 1, :b nil, :c false})))) diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index 4eefcb72..75305021 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -32,16 +32,16 @@ :handler (fn [{{{:keys [x y]} :query {:keys [z]} :path} :parameters}] {:status 200, :body {:total (+ x y z)}})} - :post {:summary "plus with body" + :post {:summary "plus with body" :parameters {:body [int?] :path {:z int?}} - :swagger {:responses {400 {:schema {:type "string"} - :description "kosh"}}} - :responses {200 {:body {:total int?}} - 500 {:description "fail"}} - :handler (fn [{{{:keys [z]} :path - xs :body} :parameters}] - {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] + :swagger {:responses {400 {:schema {:type "string"} + :description "kosh"}}} + :responses {200 {:body {:total int?}} + 500 {:description "fail"}} + :handler (fn [{{{:keys [z]} :path + xs :body} :parameters}] + {:status 200, :body {:total (+ (reduce + xs) z)}})}}]] ["/schema" {:coercion schema/coercion} ["/plus/*z" @@ -72,8 +72,8 @@ (is (= {:body {:total 7}, :status 200} (app {:request-method :post - :uri "/api/spec/plus/3" - :body-params [1 3]})))) + :uri "/api/spec/plus/3" + :body-params [1 3]})))) (testing "schema" (is (= {:body {:total 6}, :status 200} (app @@ -142,28 +142,28 @@ :description "kosh"} 500 {:description "fail"}} :summary "plus"} - :post {:parameters [{:in "body", - :name "", + :post {:parameters [{:in "body", + :name "", :description "", - :required true, - :schema {:type "array", - :items {:type "integer", - :format "int64"}}} - {:in "path" - :name "z" + :required true, + :schema {:type "array", + :items {:type "integer", + :format "int64"}}} + {:in "path" + :name "z" :description "" - :type "integer" - :required true - :format "int64"}] - :responses {200 {:description "" - :schema {:properties {"total" {:format "int64" - :type "integer"}} - :required ["total"] - :type "object"}} - 400 {:schema {:type "string"} - :description "kosh"} - 500 {:description "fail"}} - :summary "plus with body"}}}}] + :type "integer" + :required true + :format "int64"}] + :responses {200 {:description "" + :schema {:properties {"total" {:format "int64" + :type "integer"}} + :required ["total"] + :type "object"}} + 400 {:schema {:type "string"} + :description "kosh"} + 500 {:description "fail"}} + :summary "plus with body"}}}}] (is (= expected spec)) (testing "ring-async swagger-spec" From bf068d22d9e9947271f07adbfacc54f001d8c31b Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 17:20:27 +0200 Subject: [PATCH 13/51] reset compojure indentation --- .../clj/reitit/opensensors_perf_test.clj | 142 +++++++++--------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 60b69203..cd7fe781 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -237,78 +237,78 @@ (def opensensors-compojure-routes (routes (context "/v1" [] - (context "/public" [] - (ANY "/topics/:topic" [] {:name :test/route4} handler) - (ANY "/users/:user-id" [] {:name :test/route16} handler) - (ANY "/orgs/:org-id" [] {:name :test/route18} handler)) - (context "/users/:user-id" [] - (ANY "/orgs/:org-id" [] {:name :test/route5} handler) - (ANY "/invitations" [] {:name :test/route7} handler) - (ANY "/topics" [] {:name :test/route9} handler) - (ANY "/bookmarks/followers" [] {:name :test/route10} handler) - (context "/devices" [] - (ANY "/" [] {:name :test/route15} handler) - #_(ANY "/bulk" [] {:name :test/route21} handler) - (ANY "/:client-id" [] {:name :test/route35} handler) - (ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) - (ANY "/device-errors" [] {:name :test/route22} handler) - (ANY "/usage-stats" [] {:name :test/route24} handler) - (ANY "/claim-device/:client-id" [] {:name :test/route26} handler) - (ANY "/owned-orgs" [] {:name :test/route31} handler) - (ANY "/bookmark/:topic" [] {:name :test/route33} handler) - (ANY "/" [] {:name :test/route36} handler) - (ANY "/orgs" [] {:name :test/route52} handler) - (ANY "/api-key" [] {:name :test/route43} handler) - (ANY "/bookmarks" [] {:name :test/route56} handler)) - (ANY "/search/topics/:term" [] {:name :test/route6} handler) - (context "/orgs" [] - (ANY "/" [] {:name :test/route55} handler) - (context "/:org-id" [] - (context "/devices" [] - (ANY "/" [] {:name :test/route37} handler) - (ANY "/:device-id" [] {:name :test/route13} handler) - #_(ANY "/:batch/:type" [] {:name :test/route8} handler)) - (ANY "/usage-stats" [] {:name :test/route12} handler) - (ANY "/invitations" [] {:name :test/route19} handler) - (context "/members" [] - (ANY "/:user-id" [] {:name :test/route34} handler) - (ANY "/" [] {:name :test/route38} handler) - #_(ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) - (ANY "/errors" [] {:name :test/route17} handler) - (ANY "/" [] {:name :test/route42} handler) - (ANY "/confirm-membership/:token" [] {:name :test/route46} handler) - (ANY "/topics" [] {:name :test/route57} handler))) - (context "/messages" [] - (ANY "/user/:user-id" [] {:name :test/route14} handler) - (ANY "/device/:client-id" [] {:name :test/route30} handler) - (ANY "/topic/:topic" [] {:name :test/route48} handler)) - (context "/topics" [] - (ANY "/:topic" [] {:name :test/route32} handler) - (ANY "/" [] {:name :test/route54} handler)) - (ANY "/whoami" [] {:name :test/route41} handler) - (ANY "/login" [] {:name :test/route51} handler)) + (context "/public" [] + (ANY "/topics/:topic" [] {:name :test/route4} handler) + (ANY "/users/:user-id" [] {:name :test/route16} handler) + (ANY "/orgs/:org-id" [] {:name :test/route18} handler)) + (context "/users/:user-id" [] + (ANY "/orgs/:org-id" [] {:name :test/route5} handler) + (ANY "/invitations" [] {:name :test/route7} handler) + (ANY "/topics" [] {:name :test/route9} handler) + (ANY "/bookmarks/followers" [] {:name :test/route10} handler) + (context "/devices" [] + (ANY "/" [] {:name :test/route15} handler) + #_(ANY "/bulk" [] {:name :test/route21} handler) + (ANY "/:client-id" [] {:name :test/route35} handler) + (ANY "/:client-id/reset-password" [] {:name :test/route49} handler)) + (ANY "/device-errors" [] {:name :test/route22} handler) + (ANY "/usage-stats" [] {:name :test/route24} handler) + (ANY "/claim-device/:client-id" [] {:name :test/route26} handler) + (ANY "/owned-orgs" [] {:name :test/route31} handler) + (ANY "/bookmark/:topic" [] {:name :test/route33} handler) + (ANY "/" [] {:name :test/route36} handler) + (ANY "/orgs" [] {:name :test/route52} handler) + (ANY "/api-key" [] {:name :test/route43} handler) + (ANY "/bookmarks" [] {:name :test/route56} handler)) + (ANY "/search/topics/:term" [] {:name :test/route6} handler) + (context "/orgs" [] + (ANY "/" [] {:name :test/route55} handler) + (context "/:org-id" [] + (context "/devices" [] + (ANY "/" [] {:name :test/route37} handler) + (ANY "/:device-id" [] {:name :test/route13} handler) + #_(ANY "/:batch/:type" [] {:name :test/route8} handler)) + (ANY "/usage-stats" [] {:name :test/route12} handler) + (ANY "/invitations" [] {:name :test/route19} handler) + (context "/members" [] + (ANY "/:user-id" [] {:name :test/route34} handler) + (ANY "/" [] {:name :test/route38} handler) + #_(ANY "/invitation-data/:user-id" [] {:name :test/route39} handler)) + (ANY "/errors" [] {:name :test/route17} handler) + (ANY "/" [] {:name :test/route42} handler) + (ANY "/confirm-membership/:token" [] {:name :test/route46} handler) + (ANY "/topics" [] {:name :test/route57} handler))) + (context "/messages" [] + (ANY "/user/:user-id" [] {:name :test/route14} handler) + (ANY "/device/:client-id" [] {:name :test/route30} handler) + (ANY "/topic/:topic" [] {:name :test/route48} handler)) + (context "/topics" [] + (ANY "/:topic" [] {:name :test/route32} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/whoami" [] {:name :test/route41} handler) + (ANY "/login" [] {:name :test/route51} handler)) (context "/v2" [] - (ANY "/whoami" [] {:name :test/route1} handler) - (context "/users/:user-id" [] - (ANY "/datasets" [] {:name :test/route2} handler) - (ANY "/devices" [] {:name :test/route25} handler) - (context "/topics" [] - (ANY "/bulk" [] {:name :test/route29} handler) - (ANY "/" [] {:name :test/route54} handler)) - (ANY "/" [] {:name :test/route45} handler)) - (context "/public" [] - (context "/projects/:project-id" [] - (ANY "/datasets" [] {:name :test/route3} handler) - (ANY "/" [] {:name :test/route27} handler)) - #_(ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) - (ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) - (ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) - (ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) - (ANY "/login" [] {:name :test/route23} handler) - (ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) - (ANY "/schemas" [] {:name :test/route44} handler) - (ANY "/topics/:topic" [] {:name :test/route47} handler) - (ANY "/topics" [] {:name :test/route50} handler)))) + (ANY "/whoami" [] {:name :test/route1} handler) + (context "/users/:user-id" [] + (ANY "/datasets" [] {:name :test/route2} handler) + (ANY "/devices" [] {:name :test/route25} handler) + (context "/topics" [] + (ANY "/bulk" [] {:name :test/route29} handler) + (ANY "/" [] {:name :test/route54} handler)) + (ANY "/" [] {:name :test/route45} handler)) + (context "/public" [] + (context "/projects/:project-id" [] + (ANY "/datasets" [] {:name :test/route3} handler) + (ANY "/" [] {:name :test/route27} handler)) + #_(ANY "/messages/dataset/bulk" [] {:name :test/route20} handler) + (ANY "/datasets/:dataset-id" [] {:name :test/route28} handler) + (ANY "/messages/dataset/:dataset-id" [] {:name :test/route53} handler)) + (ANY "/datasets/:dataset-id" [] {:name :test/route11} handler) + (ANY "/login" [] {:name :test/route23} handler) + (ANY "/orgs/:org-id/topics" [] {:name :test/route40} handler) + (ANY "/schemas" [] {:name :test/route44} handler) + (ANY "/topics/:topic" [] {:name :test/route47} handler) + (ANY "/topics" [] {:name :test/route50} handler)))) (def opensensors-pedestal-routes (map-tree/router From 1d6cec71480fdd304276f62582bd17a293f607ec Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 17:52:28 +0200 Subject: [PATCH 14/51] +20% tps, +30% faster wildcard routing --- perf-test/clj/reitit/calf_perf_test.clj | 36 ++++++++--------------- perf-test/clj/reitit/go_perf_test.clj | 5 +++- perf-test/clj/reitit/nodejs_perf_test.clj | 2 +- project.clj | 7 +++-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj index 6609f0b1..55a0cb52 100644 --- a/perf-test/clj/reitit/calf_perf_test.clj +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -5,8 +5,7 @@ [reitit.impl] [clojure.edn :as edn] [reitit.ring :as ring] - [reitit.core :as r]) - (:import (reitit SegmentTrie))) + [reitit.core :as r])) ;; ;; start repl with `lein perf repl` @@ -87,34 +86,23 @@ {:inject-match? false, :inject-router? false})) (comment - (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)) + (let [request {:request-method :get, :uri "/user/1234/profile/compact/"}] + ;; 1338ns (old) + ;; 981ns (new) + ;; 805ns (java) + ;; 704ns (no-inject) + ;; 458ns (trie) + (cc/quick-bench + (handler-reitit request)) (handler-reitit request))) - (comment - ;; 281ns + ;; 190ns (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")) + (cc/quick-bench + (r/match-by-path router "/user/1234/profile/compact")) (r/match-by-path router "/user/1234/profile/compact"))) -(comment - (edn/read-string - (str - (.matcher - (doto (SegmentTrie.) - (.add "/user" 1) - (.add "/user/:id" 2) - (.add "/user/:id/orders" 3) - (.add "/user/id/permissions" 4)))))) - diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index a2961bc1..1a6342ce 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -300,7 +300,7 @@ (ring/create-default-handler) {:inject-match? false, :inject-router? false})) -(defrecord Req [uri request-method]) +(defrecord Req [uri request-method path-params]) (defn route->req [route] (map->Req {:request-method (-> route keys first str/lower-case keyword) @@ -317,6 +317,7 @@ ;; 120ns (faster decode params) ;; 140µs (java-segment-router) ;; 60ns (java-segment-router, no injects) + ;; 55ns (trie-router, no injects) (let [req (map->Req {:request-method :get, :uri "/user/repos"})] (title "static") (assert (= {:status 200, :body "/user/repos"} (app req))) @@ -328,6 +329,7 @@ ;; 560µs (java-segment-router) ;; 490ns (java-segment-router, no injects) ;; 440ns (java-segment-router, no injects, single-wild-optimization) + ;; 305ns (trie-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))) @@ -339,6 +341,7 @@ ;; 120µs (java-segment-router) ;; 100µs (java-segment-router, no injects) ;; 90µs (java-segment-router, no injects, single-wild-optimization) + ;; 66µs (trie-router, no injects) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/nodejs_perf_test.clj b/perf-test/clj/reitit/nodejs_perf_test.clj index 72e6dbe8..27d664ba 100644 --- a/perf-test/clj/reitit/nodejs_perf_test.clj +++ b/perf-test/clj/reitit/nodejs_perf_test.clj @@ -69,7 +69,7 @@ ;; 25310 / 25126 "regex" - ;; 88060 / 90778 + ;; 112017 / 113811 (title "reitit") ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/product/foo ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/twenty/bar diff --git a/project.clj b/project.clj index e37c2f8d..2ccc30a8 100644 --- a/project.clj +++ b/project.clj @@ -68,10 +68,13 @@ [org.clojure/clojurescript "1.10.439"] ;; modules dependencies - ;[metosin/reitit "0.2.13"] - [meta-merge] [metosin/schema-tools] [metosin/spec-tools] + [metosin/muuntaja] + [metosin/sieppari] + [metosin/jsonista] + [lambdaisland/deep-diff] + [meta-merge] [expound "0.7.2"] [orchestra "2018.12.06-2"] From ff944c455ba272277e8d780d9918dc3f932595de Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 18:23:49 +0200 Subject: [PATCH 15/51] 30% better perf than segment-router in os-test --- .../clj/reitit/opensensors_perf_test.clj | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index cd7fe781..50ecdc47 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -570,7 +570,8 @@ ;; 662ns (prefix-tree-router) ;; 567ns (segment-router) ;; 326ns (java-segment-router) - ;; 194ms (trie) + ;; 194ns (trie) + ;; 160ns (trie, prioritized) (b! "reitit" reitit-f) ;; 2845ns @@ -581,51 +582,46 @@ ;; 806ns (decode path-parameters) ;; 735ns (maybe-map-values) ;; 474ns (java-segment-router) - ;; 373ms (trie) - #_(b! "reitit-ring" reitit-ring-f) + ;; 373ns (trie) + ;; 323ns (trie, prioritized) + (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) - #_(b! "reitit-ring-fast" reitit-ring-fast-f) + ;; 240ns (trie, prioritized) + (b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) - #_(b! "reitit-ring-linear" reitit-ring-linear-f) + (b! "reitit-ring-linear" reitit-ring-linear-f) ;; 2137ns - #_(b! "calfpath-walker" calfpath-walker-f) + (b! "calfpath-walker" calfpath-walker-f) ;; 4774ns - #_(b! "calfpath-unroll" calfpath-unroll-f) + (b! "calfpath-unroll" calfpath-unroll-f) ;; 2821ns - #_(b! "pedestal" pedestal-f) + (b! "pedestal" pedestal-f) ;; 4803ns - #_(b! "calfpath-macros" calfpath-macros-f) + (b! "calfpath-macros" calfpath-macros-f) ;; 11615ns - #_(b! "compojure" compojure-f) + (b! "compojure" compojure-f) ;; 15034ns - #_(b! "bidi" bidi-f) + (b! "bidi" bidi-f) ;; 19688ns - #_(b! "ataraxy" ataraxy-f))) + (b! "ataraxy" ataraxy-f))) (comment (bench-rest!)) -(-> opensensors-routes - trie/insert - trie/compile - trie/pretty) - -(set! *warn-on-reflection* true) - -(require '[clj-async-profiler.core :as prof]) - (comment + (set! *warn-on-reflection* true) + (require '[clj-async-profiler.core :as prof]) ;; 629ms (arraylist) ;; 409ns (transient) ;; 409ns (staticMultiMatcher) From 34d8cb0f5790645df2e5725261a5a602e87e5631 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 19:41:35 +0200 Subject: [PATCH 16/51] Test with String path --- modules/reitit-core/java-src/reitit/Trie.java | 74 ++++++++----------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index ac88937a..54ae9c62 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -13,8 +13,8 @@ import java.util.*; public class Trie { - private static String decode(char[] chars, int offset, int count, boolean hasPercent, boolean hasPlus) { - final String s = new String(chars, offset, count); + private static String decode(String ss, int begin, int end, boolean hasPercent, boolean hasPlus) { + final String s = ss.substring(begin, end); try { if (hasPercent) { return URLDecoder.decode(hasPlus ? s.replace("+", "%2B") : s, "UTF-8"); @@ -24,17 +24,18 @@ public class Trie { return s; } - private static String decode(char[] chars, int offset, int count) { + private static String decode(String s, int begin, int end) { boolean hasPercent = false; boolean hasPlus = false; - for (int j = offset; j < offset + count; j++) { - if (chars[j] == '%') { + for (int j = begin; j < end; j++) { + final char c = s.charAt(j); + if (c == '%') { hasPercent = true; - } else if (chars[j] == '+') { + } else if (c == '+') { hasPlus = true; } } - return decode(chars, offset, count, hasPercent, hasPlus); + return decode(s, begin, end, hasPercent, hasPlus); } public static class Match { @@ -54,18 +55,8 @@ public class Trie { } } - public static class Path { - final char[] value; - final int size; - - Path(String value) { - this.value = value.toCharArray(); - this.size = value.length(); - } - } - public interface Matcher { - Match match(int i, Path path, Match match); + Match match(int i, int max, String path, Match match); int depth(); } @@ -86,17 +77,16 @@ public class Trie { } @Override - public Match match(int i, Path path, Match match) { - final char[] value = path.value; - if (path.size < i + size) { + public Match match(int i, int max, String path, Match match) { + if (max < i + size) { return null; } for (int j = 0; j < size; j++) { - if (value[j + i] != this.path[j]) { + if (path.charAt(j + i) != this.path[j]) { return null; } } - return child.match(i + size, path, match); + return child.match(i + size, max, path, match); } @Override @@ -122,8 +112,8 @@ public class Trie { } @Override - public Match match(int i, Path path, Match match) { - if (i == path.size) { + public Match match(int i, int max, String path, Match match) { + if (i == max) { match.data = data; return match; } @@ -155,27 +145,27 @@ public class Trie { } @Override - public Match match(int i, Path path, Match match) { - final char[] value = path.value; - if (i < path.size && value[i] != '/') { + public Match match(int i, int max, String path, Match match) { + if (i < max && path.charAt(i) != '/') { boolean hasPercent = false; boolean hasPlus = false; - for (int j = i; j < path.size; j++) { - if (value[j] == '/') { - final Match m = child.match(j, path, match); + for (int j = i; j < max; j++) { + final char c = path.charAt(j); + if (c == '/') { + final Match m = child.match(j, max, path, match); if (m != null) { - m.params.assoc(key, decode(value, i, j - i, hasPercent, hasPlus)); + m.params.assoc(key, decode(path, i, j, hasPercent, hasPlus)); } return m; - } else if (value[j] == '%') { + } else if (c == '%') { hasPercent = true; - } else if (value[j] == '+') { + } else if (c == '+') { hasPlus = true; } } - final Match m = child.match(path.size, path, match); + final Match m = child.match(max, max, path, match); if (m != null) { - m.params.assoc(key, decode(value, i, path.size - i, hasPercent, hasPlus)); + m.params.assoc(key, decode(path, i, max, hasPercent, hasPlus)); } return m; } @@ -207,9 +197,9 @@ public class Trie { } @Override - public Match match(int i, Path path, Match match) { - if (i < path.value.length) { - match.params.assoc(parameter, decode(path.value, i, path.size - i)); + public Match match(int i, int max, String path, Match match) { + if (i < max) { + match.params.assoc(parameter, decode(path, i, max)); match.data = data; return match; } @@ -243,9 +233,9 @@ public class Trie { } @Override - public Match match(int i, Path path, Match match) { + public Match match(int i, int max, String path, Match match) { for (int j = 0; j < size; j++) { - final Match m = childs[j].match(i, path, match); + final Match m = childs[j].match(i, max, path, match); if (m != null) { return m; } @@ -265,7 +255,7 @@ public class Trie { } public static Object lookup(Matcher matcher, String path) { - return matcher.match(0, new Path(path), new Match()); + return matcher.match(0, path.length(), path, new Match()); } public static Matcher scanner(List matchers) { From 86cfacb03cef79cb9916f4ff330d4cdf4633b256 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 20:12:14 +0200 Subject: [PATCH 17/51] java8 targeg --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 2ccc30a8..358f43a6 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,7 @@ :metadata {:doc/format :markdown}} :scm {:name "git" :url "https://github.com/metosin/reitit"} - :javac-options ["-Xlint:unchecked" "-target" "1.7" "-source" "1.7"] + :javac-options ["-Xlint:unchecked" "-target" "1.8" "-source" "1.8"] :managed-dependencies [[metosin/reitit "0.2.13"] [metosin/reitit-core "0.2.13"] [metosin/reitit-spec "0.2.13"] From c87bc099b014d9380bd7b0a6c44c4213046f3d67 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 2 Feb 2019 20:13:45 +0200 Subject: [PATCH 18/51] char-array is faster, re-run tests --- doc/images/opensensors.png | Bin 37115 -> 31497 bytes modules/reitit-core/java-src/reitit/Trie.java | 37 +++++++----------- perf-test/clj/reitit/go_perf_test.clj | 6 ++- perf-test/clj/reitit/nodejs_perf_test.clj | 2 +- .../clj/reitit/opensensors_perf_test.clj | 2 + 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/doc/images/opensensors.png b/doc/images/opensensors.png index 94bf228a70ee06c67836a722884217eefdaa3cbb..9f65dff4a2b6560d5b78c573fda9e9107faf59b5 100644 GIT binary patch delta 28020 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 matchers) { @@ -263,13 +263,6 @@ public class Trie { } public static void main(String[] args) { - - //Matcher matcher = new StaticMatcher("/kikka", new StaticMatcher("/kukka", new DataMatcher(1))); -// Matcher matcher = -// staticMatcher("/kikka/", -// wildMatcher(Keyword.intern("kukka"), -// staticMatcher("/kikka", -// dataMatcher(1)))); Matcher matcher = linearMatcher( Arrays.asList( diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 1a6342ce..36678094 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -20,8 +20,7 @@ ;; (defn h [path] - (fn [_] - {:status 200, :body path})) + (constantly {:status 200, :body path})) (defn add [handler routes route] (let [method (-> route keys first str/lower-case keyword) @@ -318,6 +317,7 @@ ;; 140µs (java-segment-router) ;; 60ns (java-segment-router, no injects) ;; 55ns (trie-router, no injects) + ;; 54µs (trie-router, no injects, optimized) (let [req (map->Req {:request-method :get, :uri "/user/repos"})] (title "static") (assert (= {:status 200, :body "/user/repos"} (app req))) @@ -330,6 +330,7 @@ ;; 490ns (java-segment-router, no injects) ;; 440ns (java-segment-router, no injects, single-wild-optimization) ;; 305ns (trie-router, no injects) + ;; 281µs (trie-router, no injects, optimized) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -342,6 +343,7 @@ ;; 100µs (java-segment-router, no injects) ;; 90µs (java-segment-router, no injects, single-wild-optimization) ;; 66µs (trie-router, no injects) + ;; 64µs (trie-router, no injects, optimized) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/nodejs_perf_test.clj b/perf-test/clj/reitit/nodejs_perf_test.clj index 27d664ba..7b30f118 100644 --- a/perf-test/clj/reitit/nodejs_perf_test.clj +++ b/perf-test/clj/reitit/nodejs_perf_test.clj @@ -69,7 +69,7 @@ ;; 25310 / 25126 "regex" - ;; 112017 / 113811 + ;; 112719 / 113959 (title "reitit") ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/product/foo ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:2048/twenty/bar diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 50ecdc47..36e02fe0 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -584,12 +584,14 @@ ;; 474ns (java-segment-router) ;; 373ns (trie) ;; 323ns (trie, prioritized) + ;; 289ns (trie, prioritized, zero-copy) (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) ;; 240ns (trie, prioritized) (b! "reitit-ring-fast" reitit-ring-fast-f) + ;; 240ns (trie, prioritized, zero-copy) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) From c05fe8be271bcdbd0d2f591c744346d059f1c2b0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 3 Feb 2019 10:51:00 +0200 Subject: [PATCH 19/51] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec12e4c..0b0c644a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## 0.3.0-SNAPSHOT +### `reitit.core` + +* welcome new wildcard routing! + * optional bracket-syntax with parameters + * `"/user/:user-id"` = `"/user/{user-id}"` + * `"/assets/*asset"` = `"/assets/{*asset}` + * enabling qualified parameters + * `"/user/{my.user/id}/{my.order/id}"` + * parameters don't have to span whole segments + * `"/file-:id/topics"` (free start, ends at slash) + * `"/file-{name}.html"` (free start & end) + * backed by a new `:trie-router`, replacing `:segment-router` + * [over 40% faster](https://metosin.github.io/reitit/performance.html) on the JVM + ## `reitit-frontend` * **BREAKING** New frontend controllers: From ef18a4307edfc6f87518dba6ff4cee8d35b79263 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 3 Feb 2019 15:16:55 +0200 Subject: [PATCH 20/51] format --- examples/ring-swagger/src/example/server.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj index c79f0db2..17628614 100644 --- a/examples/ring-swagger/src/example/server.clj +++ b/examples/ring-swagger/src/example/server.clj @@ -84,7 +84,7 @@ (swagger-ui/create-swagger-ui-handler {:path "/" :config {:validatorUrl nil - :operationsSorter "alpha"}}) + :operationsSorter "alpha"}}) (ring/create-default-handler)))) (defn start [] From 907e0b5c977c49f2e86125d09132772b50d42d4c Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 3 Feb 2019 15:18:12 +0200 Subject: [PATCH 21/51] dead code --- test/cljc/reitit/core_test.cljc | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index a845f6d3..19e061e8 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -333,13 +333,3 @@ (let [router (r/router ["/endpoint" (->Named :kikka)])] (is (= [["/endpoint" {:name :kikka}]] (r/routes router))))) - -(r/router - [["/:abba" ::abba] - ["/abba/1" ::abba2] - ["/:jabba/2" ::jabba2] - ["/:abba/:dabba/doo" ::doo] - ["/abba/dabba/boo/baa" ::baa] - ["/abba/:dabba/boo" ::boo] - ["/:jabba/:dabba/:doo/:daa/*foo" ::wild]] - {:router r/trie-router}) From fbf27860938fd330d3a0fbaebb59e1eaad359528 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 3 Feb 2019 15:22:59 +0200 Subject: [PATCH 22/51] disable lein-virigil --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 358f43a6..b72950d4 100644 --- a/project.clj +++ b/project.clj @@ -38,7 +38,7 @@ [io.pedestal/pedestal.service "0.5.5"]] :plugins [[jonase/eastwood "0.3.4"] - [lein-virgil "0.1.7"] + ;[lein-virgil "0.1.7"] [lein-doo "0.1.11"] [lein-cljsbuild "1.1.7"] [lein-cloverage "1.0.13"] From cecd6cf52630e48ac53118e64d0bf25750c2f9f2 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 3 Feb 2019 18:09:38 +0200 Subject: [PATCH 23/51] Start working on cljs trie --- modules/reitit-core/java-src/reitit/Trie.java | 4 -- modules/reitit-core/src/reitit/core.cljc | 4 +- modules/reitit-core/src/reitit/impl.cljc | 5 +- modules/reitit-core/src/reitit/trie.cljc | 36 ++++++++++---- .../reitit-swagger/src/reitit/swagger.cljc | 5 +- test/cljc/reitit/impl_test.cljc | 11 ----- test/cljc/reitit/trie_test.cljc | 49 ++++++++++++------- 7 files changed, 63 insertions(+), 51 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 78272b4a..656999a3 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -258,10 +258,6 @@ public class Trie { return matcher.match(0, path.length(), path.toCharArray(), new Match()); } - public static Matcher scanner(List matchers) { - return new LinearMatcher(matchers); - } - public static void main(String[] args) { Matcher matcher = linearMatcher( diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 7c2c8919..15d6eca7 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -115,7 +115,7 @@ [[] {}] compiled-routes) lookup (impl/fast-map nl) - scanner (trie/scanner pl) + matcher (trie/linear-matcher pl) routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify @@ -131,7 +131,7 @@ (route-names [_] names) (match-by-path [_ path] - (if-let [match (trie/lookup scanner path)] + (if-let [match (trie/lookup matcher path)] (-> (:data match) (assoc :path-params (:path-params match)) (assoc :path path)))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index aa33e705..38727802 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -9,13 +9,10 @@ (java.util HashMap Map) (java.net URLEncoder URLDecoder)))) -(defn normalize [s] - (-> s (trie/split-path) (trie/join-path))) - (defrecord Route [path path-parts path-params]) (defn parse [path] - (let [path #?(:clj (.intern ^String (normalize path)) :cljs (normalize path)) + (let [path #?(:clj (.intern ^String (trie/normalize path)) :cljs (trie/normalize path)) path-parts (trie/split-path path) path-params (->> path-parts (remove string?) (map :value) set)] (map->Route {:path-params path-params diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 46b0a3a6..0a7954a5 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -11,6 +11,9 @@ (defn wild? [x] (instance? Wild x)) (defn catch-all? [x] (instance? CatchAll x)) +(defprotocol Matcher + (match [this i max path match])) + ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix (defn common-prefix [s1 s2] (let [max (min (count s1) (count s2))] @@ -58,6 +61,9 @@ (instance? CatchAll x) (str "{*" (-> x :value str (subs 1)) "}")))) "" xs)) +(defn normalize [s] + (-> s (split-path) (join-path))) + (defn- -node [m] (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) @@ -103,6 +109,21 @@ (update :children dissoc "")) node'))) +(defn data-matcher [data] + #?(:clj (Trie/dataMatcher data))) + +(defn static-matcher [path matcher] + #?(:clj (Trie/staticMatcher path matcher))) + +(defn wild-matcher [path matcher] + #?(:clj (Trie/wildMatcher path matcher))) + +(defn catch-all-matcher [path data] + #?(:clj (Trie/catchAllMatcher path data))) + +(defn linear-matcher [matchers] + #?(:clj (Trie/linearMatcher matchers))) + ;; ;; public api ;; @@ -118,14 +139,14 @@ ([node path data] (-insert (or node (-node {})) (split-path path) data))) -(defn ^Trie$Matcher compile [{:keys [data children wilds catch-all]}] +(defn compile [{:keys [data children wilds catch-all]}] (let [matchers (cond-> [] - data (conj (Trie/dataMatcher data)) - children (into (for [[p c] children] (Trie/staticMatcher p (compile c)))) - wilds (into (for [[p c] wilds] (Trie/wildMatcher p (compile c)))) - catch-all (into (for [[p c] catch-all] (Trie/catchAllMatcher p (:data c)))))] + data (conj (data-matcher data)) + children (into (for [[p c] children] (static-matcher p (compile c)))) + wilds (into (for [[p c] wilds] (wild-matcher p (compile c)))) + catch-all (into (for [[p c] catch-all] (catch-all-matcher p (:data c)))))] (if (rest matchers) - (Trie/linearMatcher matchers) + (linear-matcher matchers) (first matchers)))) (defn pretty [matcher] @@ -135,9 +156,6 @@ (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] (->Match (.data match) (.parameters match)))) -(defn scanner [compiled-tries] - (Trie/scanner compiled-tries)) - ;; ;; spike ;; diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index acd70fd7..b5c58fc5 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -5,7 +5,8 @@ [clojure.spec.alpha :as s] [clojure.set :as set] [clojure.string :as str] - [reitit.coercion :as coercion])) + [reitit.coercion :as coercion] + [reitit.trie :as trie])) (s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{}))) (s/def ::no-doc boolean?) @@ -65,7 +66,7 @@ :spec ::spec}) (defn- swagger-path [path] - (-> path impl/normalize (str/replace #"\{\*" "{"))) + (-> path trie/normalize (str/replace #"\{\*" "{"))) (defn create-swagger-handler [] "Create a ring handler to emit swagger spec. Collects all routes from router which have diff --git a/test/cljc/reitit/impl_test.cljc b/test/cljc/reitit/impl_test.cljc index 5ad83786..3c72ab23 100644 --- a/test/cljc/reitit/impl_test.cljc +++ b/test/cljc/reitit/impl_test.cljc @@ -2,17 +2,6 @@ (:require [clojure.test :refer [deftest testing is are]] [reitit.impl :as impl])) -(deftest normalize-test - (are [path expected] - (is (= expected (impl/normalize path))) - - "/olipa/:kerran/avaruus", "/olipa/{kerran}/avaruus" - "/olipa/{kerran}/avaruus", "/olipa/{kerran}/avaruus" - "/olipa/{a.b/c}/avaruus", "/olipa/{a.b/c}/avaruus" - "/olipa/kerran/*avaruus", "/olipa/kerran/{*avaruus}" - "/olipa/kerran/{*avaruus}", "/olipa/kerran/{*avaruus}" - "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) - (deftest conflicting-route-test (are [c? p1 p2] (is (= c? (impl/conflicting-routes? [p1] [p2]))) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index 570c45c5..b56981dd 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -1,26 +1,37 @@ (ns reitit.trie-test (:require [clojure.test :refer [deftest testing is are]] - [reitit.trie :as rt])) + [reitit.trie :as trie])) + +(deftest normalize-test + (are [path expected] + (is (= expected (trie/normalize path))) + + "/olipa/:kerran/avaruus", "/olipa/{kerran}/avaruus" + "/olipa/{kerran}/avaruus", "/olipa/{kerran}/avaruus" + "/olipa/{a.b/c}/avaruus", "/olipa/{a.b/c}/avaruus" + "/olipa/kerran/*avaruus", "/olipa/kerran/{*avaruus}" + "/olipa/kerran/{*avaruus}", "/olipa/kerran/{*avaruus}" + "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) (deftest tests - (is (= (rt/->Match {:a 1} {}) - (-> (rt/insert nil "/foo" {:a 1}) - (rt/compile) - (rt/lookup "/foo")))) + (is (= (trie/->Match {:a 1} {}) + (-> (trie/insert nil "/foo" {:a 1}) + (trie/compile) + (trie/lookup "/foo")))) - (is (= (rt/->Match {:a 1} {}) - (-> (rt/insert nil "/foo" {:a 1}) - (rt/insert "/foo/*bar" {:b 1}) - (rt/compile) - (rt/lookup "/foo")))) + (is (= (trie/->Match {:a 1} {}) + (-> (trie/insert nil "/foo" {:a 1}) + (trie/insert "/foo/*bar" {:b 1}) + (trie/compile) + (trie/lookup "/foo")))) - (is (= (rt/->Match {:b 1} {:bar "bar"}) - (-> (rt/insert nil "/foo" {:a 1}) - (rt/insert "/foo/*bar" {:b 1}) - (rt/compile) - (rt/lookup "/foo/bar")))) + (is (= (trie/->Match {:b 1} {:bar "bar"}) + (-> (trie/insert nil "/foo" {:a 1}) + (trie/insert "/foo/*bar" {:b 1}) + (trie/compile) + (trie/lookup "/foo/bar")))) - (is (= (rt/->Match {:a 1} {}) - (-> (rt/insert nil "" {:a 1}) - (rt/compile) - (rt/lookup ""))))) + (is (= (trie/->Match {:a 1} {}) + (-> (trie/insert nil "" {:a 1}) + (trie/compile) + (trie/lookup ""))))) From 2eb45134479abb366518754244ce850846de3772 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 5 Feb 2019 08:50:57 +0200 Subject: [PATCH 24/51] Initial pure-clojure impl --- modules/reitit-core/src/reitit/trie.cljc | 114 ++++++++++++++++++++--- perf-test/clj/reitit/go_perf_test.clj | 18 +++- project.clj | 2 +- 3 files changed, 114 insertions(+), 20 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 0a7954a5..4cc415b3 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -1,7 +1,8 @@ (ns reitit.trie (:refer-clojure :exclude [compile]) (:require [clojure.string :as str]) - (:import [reitit Trie Trie$Match Trie$Matcher])) + (:import [reitit Trie Trie$Match Trie$Matcher] + (java.net URLDecoder))) (defrecord Wild [value]) (defrecord CatchAll [value]) @@ -12,7 +13,13 @@ (defn catch-all? [x] (instance? CatchAll x)) (defprotocol Matcher - (match [this i max path match])) + (match [this i max ^chars path]) + (view [this]) + (depth [this])) + +(defn -assoc! [match k v] + (let [params (or (:path-params match) (transient {}))] + (assoc match :path-params (assoc! params k v)))) ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix (defn common-prefix [s1 s2] @@ -41,7 +48,7 @@ (loop [ss nil, from 0, to 0] (if (= to (count s)) (concat ss (-static from to)) - (condp = (get s to) + (case (get s to) \{ (let [to' (or (str/index-of s "}" to) (throw (ex-info (str "Unbalanced brackets: " (pr-str s)) {})))] (if (= \* (get s (inc to))) (recur (concat ss (-static from to) (-catch-all (inc to) to')) (inc to') (inc to')) @@ -109,20 +116,90 @@ (update :children dissoc "")) node'))) +(set! *warn-on-reflection* true) + +(defn decode! + ([chars start end] + (let [s (String. ^chars chars ^int start ^int (- end start))] + (if (str/index-of s \%) + (URLDecoder/decode + (if (str/index-of s \+) (.replace ^String s "+" "%2B") s) + "UTF-8") + s))) + ([chars start end percent? plus?] + (let [s (String. ^chars chars ^int start ^int (- end start))] + (if percent? + (URLDecoder/decode + (if plus? (.replace ^String s "+" "%2B") s) + "UTF-8") + s)))) + (defn data-matcher [data] - #?(:clj (Trie/dataMatcher data))) + #?(:cljx (Trie/dataMatcher data) + :clj (let [match (->Match data nil)] + (reify Matcher + (match [_ i max _] + (if (= i max) + match)) + (view [_] data) + (depth [_] 1))))) -(defn static-matcher [path matcher] - #?(:clj (Trie/staticMatcher path matcher))) +(defn static-matcher [^String path matcher] + #?(:cljx (Trie/staticMatcher path matcher) + :clj (let [^chars chars (.toCharArray path) + size (alength chars)] + (reify Matcher + (match [_ i max path] + (if-not (< max (+ ^int i size)) + (loop [j 0] + (if (= j size) + (match matcher (+ ^int i size) max path) + (if (= ^char (aget ^chars path (+ ^int i j)) ^char (aget ^chars chars j)) + (recur (inc j))))))) + (view [_] [path (view matcher)]) + (depth [_] (inc (depth matcher))))))) -(defn wild-matcher [path matcher] - #?(:clj (Trie/wildMatcher path matcher))) +(defn wild-matcher [key matcher] + #?(:cljx (Trie/wildMatcher key matcher) + :clj (reify Matcher + (match [_ i max path] + (if (and (< ^int i ^int max) (not= ^char (aget ^chars path i) \/)) + (loop [percent? false, plus? false, ^int j ^int i] + (if (= ^int max j) + (if-let [match (match matcher max max path)] + (-assoc! match key (decode! path i max percent? plus?))) + (let [c ^char (aget ^chars path j)] + (case c + \/ (if-let [match (match matcher j max path)] + (-assoc! match key (decode! path i j percent? plus?))) + \% (recur true plus? (inc j)) + \+ (recur percent? true (inc j)) + (recur percent? plus? (inc j)))))))) + (view [_] [key (view matcher)]) + (depth [_] (inc (depth matcher)))))) -(defn catch-all-matcher [path data] - #?(:clj (Trie/catchAllMatcher path data))) +(defn catch-all-matcher [key data] + #?(:cljx (Trie/catchAllMatcher key data) + :clj (let [match (->Match data nil)] + (reify Matcher + (match [_ i max path] + (if (< ^int i max) + (-assoc! match key (decode! path i max)))) + (view [_] [key [data]]) + (depth [_] 1))))) (defn linear-matcher [matchers] - #?(:clj (Trie/linearMatcher matchers))) + #?(:cljx (Trie/linearMatcher matchers) + :clj (let [matchers (.toArray ^java.util.List (reverse (sort-by depth matchers))) + size (alength matchers)] + (reify Matcher + (match [_ i max path] + (loop [j 0] + (if (< j size) + (or (match (aget matchers j) i max path) + (recur (inc j)))))) + (view [_] (mapv view matchers)) + (depth [_] (apply max (map depth matchers))))))) ;; ;; public api @@ -150,11 +227,18 @@ (first matchers)))) (defn pretty [matcher] - (-> matcher str read-string eval)) + #?(:cljx (-> matcher str read-string eval) + :clj (view matcher))) -(defn lookup [^Trie$Matcher matcher path] - (if-let [match ^Trie$Match (Trie/lookup matcher ^String path)] - (->Match (.data match) (.parameters match)))) +(defn lookup [matcher ^String path] + #?(:cljx (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] + (->Match (.data match) (.parameters match))) + :clj (let [chars (.toCharArray path)] + (if-let [match (match matcher 0 (alength chars) chars)] + (let [params (if-let [path-params (:path-params match)] + (persistent! path-params) + {})] + (assoc match :path-params params)))))) ;; ;; spike diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 36678094..37259310 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -317,7 +317,7 @@ ;; 140µs (java-segment-router) ;; 60ns (java-segment-router, no injects) ;; 55ns (trie-router, no injects) - ;; 54µs (trie-router, no injects, optimized) + ;; 54ns (trie-router, no injects, optimized) (let [req (map->Req {:request-method :get, :uri "/user/repos"})] (title "static") (assert (= {:status 200, :body "/user/repos"} (app req))) @@ -330,7 +330,7 @@ ;; 490ns (java-segment-router, no injects) ;; 440ns (java-segment-router, no injects, single-wild-optimization) ;; 305ns (trie-router, no injects) - ;; 281µs (trie-router, no injects, optimized) + ;; 281ns (trie-router, no injects, optimized) - 690ns (clojure) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -343,7 +343,7 @@ ;; 100µs (java-segment-router, no injects) ;; 90µs (java-segment-router, no injects, single-wild-optimization) ;; 66µs (trie-router, no injects) - ;; 64µs (trie-router, no injects, optimized) + ;; 64µs (trie-router, no injects, optimized) - 124µs (clojure) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench @@ -351,4 +351,14 @@ (app r))))) (comment - (routing-test)) + (routing-test) + (ring/get-router app) + (app {:uri "/authorizations/1", :request-method :get}) + (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}) + (do + (require '[clj-async-profiler.core :as prof]) + (prof/start {}) + (time + (dotimes [_ 10000000] + (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}))) + (str (prof/stop {})))) diff --git a/project.clj b/project.clj index b72950d4..348c6ccb 100644 --- a/project.clj +++ b/project.clj @@ -97,7 +97,7 @@ [manifold "0.1.8"] [funcool/promesa "1.9.0"] - [com.clojure-goes-fast/clj-async-profiler "0.2.3-SNAPSHOT"] + [com.clojure-goes-fast/clj-async-profiler "0.3.0"] ;; https://github.com/bensu/doo/issues/180 [fipp "0.6.14" :exclusions [org.clojure/core.rrb-vector]]]} From 5b9f90d283d3762e016d137b0b757ca5ac2618e1 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 5 Feb 2019 08:51:21 +0200 Subject: [PATCH 25/51] ifs -> switch. maybe faster? --- modules/reitit-core/java-src/reitit/Trie.java | 39 ++++++++++++------- perf-test/clj/reitit/go_perf_test.clj | 11 +++--- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 656999a3..e994dd4d 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -28,11 +28,15 @@ public class Trie { boolean hasPercent = false; boolean hasPlus = false; for (int j = begin; j < end; j++) { - final char c = chars[j]; - if (c == '%') { - hasPercent = true; - } else if (c == '+') { - hasPlus = true; + switch (chars[j]) { + case '%': + hasPercent = true; + break; + case '+': + hasPlus = true; + break; + default: + break; } } return decode(chars, begin, end, hasPercent, hasPlus); @@ -151,16 +155,21 @@ public class Trie { boolean hasPlus = false; for (int j = i; j < max; j++) { final char c = path[j]; - if (c == '/') { - final Match m = child.match(j, max, path, match); - if (m != null) { - m.params.assoc(key, decode(path, i, j, hasPercent, hasPlus)); - } - return m; - } else if (c == '%') { - hasPercent = true; - } else if (c == '+') { - hasPlus = true; + switch (c) { + case '/': + final Match m = child.match(j, max, path, match); + if (m != null) { + m.params.assoc(key, decode(path, i, j, hasPercent, hasPlus)); + } + return m; + case '%': + hasPercent = true; + break; + case '+': + hasPlus = true; + break; + default: + break; } } final Match m = child.match(max, max, path, match); diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 37259310..f62983de 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -330,7 +330,8 @@ ;; 490ns (java-segment-router, no injects) ;; 440ns (java-segment-router, no injects, single-wild-optimization) ;; 305ns (trie-router, no injects) - ;; 281ns (trie-router, no injects, optimized) - 690ns (clojure) + ;; 281ns (trie-router, no injects, optimized) + ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -344,6 +345,7 @@ ;; 90µs (java-segment-router, no injects, single-wild-optimization) ;; 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) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench @@ -357,8 +359,7 @@ (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}) (do (require '[clj-async-profiler.core :as prof]) - (prof/start {}) - (time - (dotimes [_ 10000000] + (prof/profile + (dotimes [_ 1000000] (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}))) - (str (prof/stop {})))) + (prof/serve-files 8080))) From 54d5550faeacd8be372ca786be0d7c5fbd608727 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Fri, 8 Feb 2019 19:17:22 +0200 Subject: [PATCH 26/51] cljs trie --- modules/reitit-core/src/reitit/trie.cljc | 232 ++++++++++++----------- 1 file changed, 117 insertions(+), 115 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 4cc415b3..f1f1f376 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -1,8 +1,8 @@ (ns reitit.trie - (:refer-clojure :exclude [compile]) + (:refer-clojure :exclude [compile -assoc!]) (:require [clojure.string :as str]) - (:import [reitit Trie Trie$Match Trie$Matcher] - (java.net URLDecoder))) + #?(:clj (:import [reitit Trie Trie$Match Trie$Matcher] + (java.net URLDecoder)))) (defrecord Wild [value]) (defrecord CatchAll [value]) @@ -13,7 +13,7 @@ (defn catch-all? [x] (instance? CatchAll x)) (defprotocol Matcher - (match [this i max ^chars path]) + (match [this i max path]) (view [this]) (depth [this])) @@ -116,27 +116,27 @@ (update :children dissoc "")) node'))) -(set! *warn-on-reflection* true) - (defn decode! - ([chars start end] - (let [s (String. ^chars chars ^int start ^int (- end start))] - (if (str/index-of s \%) - (URLDecoder/decode - (if (str/index-of s \+) (.replace ^String s "+" "%2B") s) - "UTF-8") - s))) - ([chars start end percent? plus?] - (let [s (String. ^chars chars ^int start ^int (- end start))] - (if percent? - (URLDecoder/decode - (if plus? (.replace ^String s "+" "%2B") s) - "UTF-8") - s)))) + ([path start end] + #?(:clj (let [s (subs path start end)] + (if (str/index-of s \%) + (URLDecoder/decode + (if (str/index-of s \+) (.replace ^String s "+" "%2B") s) + "UTF-8") + s)) + :cljs (js/decodeURIComponent (subs path start end)))) + ([path start end percent? plus?] + #?(:clj (let [s (String. ^chars path ^int start ^int (- end start))] + (if percent? + (URLDecoder/decode + (if plus? (.replace ^String s "+" "%2B") s) + "UTF-8") + s)) + :cljs (js/decodeURIComponent (subs path start end))))) (defn data-matcher [data] - #?(:cljx (Trie/dataMatcher data) - :clj (let [match (->Match data nil)] + #?(:clj (Trie/dataMatcher data) + :cljs (let [match (->Match data nil)] (reify Matcher (match [_ i max _] (if (= i max) @@ -144,31 +144,30 @@ (view [_] data) (depth [_] 1))))) -(defn static-matcher [^String path matcher] - #?(:cljx (Trie/staticMatcher path matcher) - :clj (let [^chars chars (.toCharArray path) - size (alength chars)] +(defn static-matcher [path matcher] + #?(:clj (Trie/staticMatcher ^String path ^Trie$Matcher matcher) + :cljs (let [size (count path)] (reify Matcher - (match [_ i max path] - (if-not (< max (+ ^int i size)) + (match [_ i max p] + (if-not (< max (+ i size)) (loop [j 0] (if (= j size) - (match matcher (+ ^int i size) max path) - (if (= ^char (aget ^chars path (+ ^int i j)) ^char (aget ^chars chars j)) + (match matcher (+ i size) max p) + (if (= (get p (+ i j)) (get path j)) (recur (inc j))))))) (view [_] [path (view matcher)]) (depth [_] (inc (depth matcher))))))) (defn wild-matcher [key matcher] - #?(:cljx (Trie/wildMatcher key matcher) - :clj (reify Matcher + #?(:clj (Trie/wildMatcher key matcher) + :cljs (reify Matcher (match [_ i max path] - (if (and (< ^int i ^int max) (not= ^char (aget ^chars path i) \/)) - (loop [percent? false, plus? false, ^int j ^int i] - (if (= ^int max j) + (if (and (< i max) (not= (get path i) \/)) + (loop [percent? false, plus? false, j i] + (if (= max j) (if-let [match (match matcher max max path)] (-assoc! match key (decode! path i max percent? plus?))) - (let [c ^char (aget ^chars path j)] + (let [c ^char (get path j)] (case c \/ (if-let [match (match matcher j max path)] (-assoc! match key (decode! path i j percent? plus?))) @@ -179,24 +178,24 @@ (depth [_] (inc (depth matcher)))))) (defn catch-all-matcher [key data] - #?(:cljx (Trie/catchAllMatcher key data) - :clj (let [match (->Match data nil)] + #?(:clj (Trie/catchAllMatcher key data) + :cljs (let [match (->Match data nil)] (reify Matcher (match [_ i max path] - (if (< ^int i max) + (if (< i max) (-assoc! match key (decode! path i max)))) (view [_] [key [data]]) (depth [_] 1))))) (defn linear-matcher [matchers] - #?(:cljx (Trie/linearMatcher matchers) - :clj (let [matchers (.toArray ^java.util.List (reverse (sort-by depth matchers))) - size (alength matchers)] + #?(:clj (Trie/linearMatcher matchers) + :cljs (let [matchers (vec (reverse (sort-by depth matchers))) + size (count matchers)] (reify Matcher (match [_ i max path] (loop [j 0] (if (< j size) - (or (match (aget matchers j) i max path) + (or (match (get matchers j) i max path) (recur (inc j)))))) (view [_] (mapv view matchers)) (depth [_] (apply max (map depth matchers))))))) @@ -227,85 +226,88 @@ (first matchers)))) (defn pretty [matcher] - #?(:cljx (-> matcher str read-string eval) - :clj (view matcher))) + #?(:clj (-> matcher str read-string eval) + :cljs (view matcher))) -(defn lookup [matcher ^String path] - #?(:cljx (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] +(defn lookup [matcher path] + #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] (->Match (.data match) (.parameters match))) - :clj (let [chars (.toCharArray path)] - (if-let [match (match matcher 0 (alength chars) chars)] - (let [params (if-let [path-params (:path-params match)] - (persistent! path-params) - {})] - (assoc match :path-params params)))))) + :cljs (if-let [match (match matcher 0 (count path) path)] + (let [params (if-let [path-params (:path-params match)] + (persistent! path-params) + {})] + (assoc match :path-params params))))) ;; ;; spike ;; -(-> - [["/v2/whoami" 1] - ["/v2/users/:user-id/datasets" 2] - ["/v2/public/projects/:project-id/datasets" 3] - ["/v1/public/topics/:topic" 4] - ["/v1/users/:user-id/orgs/:org-id" 5] - ["/v1/search/topics/:term" 6] - ["/v1/users/:user-id/invitations" 7] - ["/v1/users/:user-id/topics" 9] - ["/v1/users/:user-id/bookmarks/followers" 10] - ["/v2/datasets/:dataset-id" 11] - ["/v1/orgs/:org-id/usage-stats" 12] - ["/v1/orgs/:org-id/devices/:client-id" 13] - ["/v1/messages/user/:user-id" 14] - ["/v1/users/:user-id/devices" 15] - ["/v1/public/users/:user-id" 16] - ["/v1/orgs/:org-id/errors" 17] - ["/v1/public/orgs/:org-id" 18] - ["/v1/orgs/:org-id/invitations" 19] - ["/v1/users/:user-id/device-errors" 22] - ["/v2/login" 23] - ["/v1/users/:user-id/usage-stats" 24] - ["/v2/users/:user-id/devices" 25] - ["/v1/users/:user-id/claim-device/:client-id" 26] - ["/v2/public/projects/:project-id" 27] - ["/v2/public/datasets/:dataset-id" 28] - ["/v2/users/:user-id/topics/bulk" 29] - ["/v1/messages/device/:client-id" 30] - ["/v1/users/:user-id/owned-orgs" 31] - ["/v1/topics/:topic" 32] - ["/v1/users/:user-id/bookmark/:topic" 33] - ["/v1/orgs/:org-id/members/:user-id" 34] - ["/v1/users/:user-id/devices/:client-id" 35] - ["/v1/users/:user-id" 36] - ["/v1/orgs/:org-id/devices" 37] - ["/v1/orgs/:org-id/members" 38] - ["/v2/orgs/:org-id/topics" 40] - ["/v1/whoami" 41] - ["/v1/orgs/:org-id" 42] - ["/v1/users/:user-id/api-key" 43] - ["/v2/schemas" 44] - ["/v2/users/:user-id/topics" 45] - ["/v1/orgs/:org-id/confirm-membership/:token" 46] - ["/v2/topics/:topic" 47] - ["/v1/messages/topic/:topic" 48] - ["/v1/users/:user-id/devices/:client-id/reset-password" 49] - ["/v2/topics" 50] - ["/v1/login" 51] - ["/v1/users/:user-id/orgs" 52] - ["/v2/public/messages/dataset/:dataset-id" 53] - ["/v1/topics" 54] - ["/v1/orgs" 55] - ["/v1/users/:user-id/bookmarks" 56] - ["/v1/orgs/:org-id/topics" 57]] - (insert) - (compile) - (pretty)) - -(-> [["/kikka" 2] - ["/kikka/kakka/kukka" 3] - ["/kikka/:kakka/kurkku" 4] - ["/kikka/kuri/{user/doc}/html" 5]] +(comment + (-> + [["/v2/whoami" 1] + ["/v2/users/:user-id/datasets" 2] + ["/v2/public/projects/:project-id/datasets" 3] + ["/v1/public/topics/:topic" 4] + ["/v1/users/:user-id/orgs/:org-id" 5] + ["/v1/search/topics/:term" 6] + ["/v1/users/:user-id/invitations" 7] + ["/v1/users/:user-id/topics" 9] + ["/v1/users/:user-id/bookmarks/followers" 10] + ["/v2/datasets/:dataset-id" 11] + ["/v1/orgs/:org-id/usage-stats" 12] + ["/v1/orgs/:org-id/devices/:client-id" 13] + ["/v1/messages/user/:user-id" 14] + ["/v1/users/:user-id/devices" 15] + ["/v1/public/users/:user-id" 16] + ["/v1/orgs/:org-id/errors" 17] + ["/v1/public/orgs/:org-id" 18] + ["/v1/orgs/:org-id/invitations" 19] + ["/v1/users/:user-id/device-errors" 22] + ["/v2/login" 23] + ["/v1/users/:user-id/usage-stats" 24] + ["/v2/users/:user-id/devices" 25] + ["/v1/users/:user-id/claim-device/:client-id" 26] + ["/v2/public/projects/:project-id" 27] + ["/v2/public/datasets/:dataset-id" 28] + ["/v2/users/:user-id/topics/bulk" 29] + ["/v1/messages/device/:client-id" 30] + ["/v1/users/:user-id/owned-orgs" 31] + ["/v1/topics/:topic" 32] + ["/v1/users/:user-id/bookmark/:topic" 33] + ["/v1/orgs/:org-id/members/:user-id" 34] + ["/v1/users/:user-id/devices/:client-id" 35] + ["/v1/users/:user-id" 36] + ["/v1/orgs/:org-id/devices" 37] + ["/v1/orgs/:org-id/members" 38] + ["/v2/orgs/:org-id/topics" 40] + ["/v1/whoami" 41] + ["/v1/orgs/:org-id" 42] + ["/v1/users/:user-id/api-key" 43] + ["/v2/schemas" 44] + ["/v2/users/:user-id/topics" 45] + ["/v1/orgs/:org-id/confirm-membership/:token" 46] + ["/v2/topics/:topic" 47] + ["/v1/messages/topic/:topic" 48] + ["/v1/users/:user-id/devices/:client-id/reset-password" 49] + ["/v2/topics" 50] + ["/v1/login" 51] + ["/v1/users/:user-id/orgs" 52] + ["/v2/public/messages/dataset/:dataset-id" 53] + ["/v1/topics" 54] + ["/v1/orgs" 55] + ["/v1/users/:user-id/bookmarks" 56] + ["/v1/orgs/:org-id/topics" 57]] (insert) (compile) (pretty)) + + (-> [["/kikka" 2] + ["/kikka/kakka/kukka" 3] + ["/kikka/:kakka/kurkku" 4] + ["/kikka/kuri/{user/doc}/html" 5]] + (insert) + (compile) + (pretty)) + + (map str (.toCharArray "\u2215\u0048\u0065\u006C\u006C\u006F")) + (count ["∕" "H" "e" "l" "l" "o" " " "W" "o" "r" "l" "d"])) From c302f795abdaa12618d4bc9fc8efdb222c75792c Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Fri, 8 Feb 2019 19:22:30 +0200 Subject: [PATCH 27/51] Remove dead code --- modules/reitit-core/src/reitit/trie.cljc | 35 +++++++----------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index f1f1f376..52584e2e 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -116,23 +116,10 @@ (update :children dissoc "")) node'))) -(defn decode! - ([path start end] - #?(:clj (let [s (subs path start end)] - (if (str/index-of s \%) - (URLDecoder/decode - (if (str/index-of s \+) (.replace ^String s "+" "%2B") s) - "UTF-8") - s)) - :cljs (js/decodeURIComponent (subs path start end)))) - ([path start end percent? plus?] - #?(:clj (let [s (String. ^chars path ^int start ^int (- end start))] - (if percent? - (URLDecoder/decode - (if plus? (.replace ^String s "+" "%2B") s) - "UTF-8") - s)) - :cljs (js/decodeURIComponent (subs path start end))))) +#?(:cljs + (defn decode! [path start end percent?] + (let [path (subs path start end)] + (if percent? (js/decodeURIComponent path) path)))) (defn data-matcher [data] #?(:clj (Trie/dataMatcher data) @@ -163,17 +150,16 @@ :cljs (reify Matcher (match [_ i max path] (if (and (< i max) (not= (get path i) \/)) - (loop [percent? false, plus? false, j i] + (loop [percent? false, j i] (if (= max j) (if-let [match (match matcher max max path)] - (-assoc! match key (decode! path i max percent? plus?))) + (-assoc! match key (decode! path i max percent?))) (let [c ^char (get path j)] (case c \/ (if-let [match (match matcher j max path)] - (-assoc! match key (decode! path i j percent? plus?))) - \% (recur true plus? (inc j)) - \+ (recur percent? true (inc j)) - (recur percent? plus? (inc j)))))))) + (-assoc! match key (decode! path i j percent?))) + \% (recur true (inc j)) + (recur percent? (inc j)))))))) (view [_] [key (view matcher)]) (depth [_] (inc (depth matcher)))))) @@ -182,8 +168,7 @@ :cljs (let [match (->Match data nil)] (reify Matcher (match [_ i max path] - (if (< i max) - (-assoc! match key (decode! path i max)))) + (if (< i max) (-assoc! match key (decode! path i max true)))) (view [_] [key [data]]) (depth [_] 1))))) From 81b9bdceef4558103208001f11dd4e0671fcd334 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 11:51:15 +0200 Subject: [PATCH 28/51] Small improvement * Sort linear routes secondary with static path length * Unwrap data-matchers from linear-router * Simplify StaticMatcher impl --- modules/reitit-core/java-src/reitit/Trie.java | 64 +++++++++++------ modules/reitit-core/src/reitit/trie.cljc | 72 +++++++++++-------- perf-test/clj/reitit/go_perf_test.clj | 2 + 3 files changed, 88 insertions(+), 50 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index e994dd4d..738a9516 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -63,6 +63,8 @@ public class Trie { Match match(int i, int max, char[] path, Match match); int depth(); + + int length(); } public static StaticMatcher staticMatcher(String path, Matcher child) { @@ -98,6 +100,11 @@ public class Trie { return child.depth() + 1; } + @Override + public int length() { + return path.length; + } + @Override public String toString() { return "[\"" + new String(path) + "\" " + child + "]"; @@ -129,52 +136,50 @@ public class Trie { return 1; } + @Override + public int length() { + return 0; + } + @Override public String toString() { return (data != null ? data.toString() : "nil"); } } - public static WildMatcher wildMatcher(Keyword parameter, Matcher child) { - return new WildMatcher(parameter, child); + public static WildMatcher wildMatcher(Keyword parameter, char end, Matcher child) { + return new WildMatcher(parameter, end, child); } static final class WildMatcher implements Matcher { private final Keyword key; + private final char end; private final Matcher child; - WildMatcher(Keyword key, Matcher child) { + WildMatcher(Keyword key, char end, Matcher child) { this.key = key; + this.end = end; this.child = child; } @Override public Match match(int i, int max, char[] path, Match match) { - if (i < max && path[i] != '/') { + if (i < max && path[i] != end) { boolean hasPercent = false; boolean hasPlus = false; + int stop = max; for (int j = i; j < max; j++) { final char c = path[j]; - switch (c) { - case '/': - final Match m = child.match(j, max, path, match); - if (m != null) { - m.params.assoc(key, decode(path, i, j, hasPercent, hasPlus)); - } - return m; - case '%': - hasPercent = true; - break; - case '+': - hasPlus = true; - break; - default: - break; + if (c == end) { + stop = j; + break; } + hasPercent = hasPercent || c == '%'; + hasPlus = hasPlus || c == '+'; } - final Match m = child.match(max, max, path, match); + final Match m = child.match(stop, max, path, match); if (m != null) { - m.params.assoc(key, decode(path, i, max, hasPercent, hasPlus)); + m.params.assoc(key, decode(path, i, stop, hasPercent, hasPlus)); } return m; } @@ -186,6 +191,11 @@ public class Trie { return child.depth() + 1; } + @Override + public int length() { + return 0; + } + @Override public String toString() { return "[" + key + " " + child + "]"; @@ -220,6 +230,11 @@ public class Trie { return 1; } + @Override + public int length() { + return 0; + } + @Override public String toString() { return "[" + parameter + " " + new DataMatcher(data) + "]"; @@ -237,7 +252,7 @@ public class Trie { LinearMatcher(List childs) { this.childs = childs.toArray(new Matcher[0]); - Arrays.sort(this.childs, Comparator.comparing(Matcher::depth).reversed()); + Arrays.sort(this.childs, Comparator.comparing(Matcher::depth).thenComparing(Matcher::length).reversed()); this.size = childs.size(); } @@ -257,6 +272,11 @@ public class Trie { return Arrays.stream(childs).mapToInt(Matcher::depth).max().orElseThrow(NoSuchElementException::new); } + @Override + public int length() { + return 0; + } + @Override public String toString() { return Arrays.toString(childs); diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 52584e2e..645c64df 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -15,7 +15,8 @@ (defprotocol Matcher (match [this i max path]) (view [this]) - (depth [this])) + (depth [this]) + (length [this])) (defn -assoc! [match k v] (let [params (or (:path-params match) (transient {}))] @@ -33,8 +34,7 @@ (not= (get s1 i) (get s2 i)) (if-not (zero? i) (subs s1 0 i)) ;; recur - :else - (recur (inc i)))))) + :else (recur (inc i)))))) (defn- -keyword [s] (if-let [i (str/index-of s "/")] @@ -81,10 +81,13 @@ (assoc node :data data) (instance? Wild path) - (update-in node [:wilds (:value path)] (fn [n] (-insert (or n (-node {})) ps data))) + (let [next (first ps)] + (if (or (instance? Wild next) (instance? CatchAll next)) + (throw (ex-info (str "Two following wilds: " path ", " next) {})) + (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))))) (instance? CatchAll path) - (assoc-in node [:catch-all (:value path)] (-node {:data data})) + (assoc-in node [:catch-all path] (-node {:data data})) (str/blank? path) (-insert node ps data) @@ -118,8 +121,7 @@ #?(:cljs (defn decode! [path start end percent?] - (let [path (subs path start end)] - (if percent? (js/decodeURIComponent path) path)))) + (if percent? (js/decodeURIComponent (subs path start end)) path))) (defn data-matcher [data] #?(:clj (Trie/dataMatcher data) @@ -129,7 +131,8 @@ (if (= i max) match)) (view [_] data) - (depth [_] 1))))) + (depth [_] 1) + (length [_]))))) (defn static-matcher [path matcher] #?(:clj (Trie/staticMatcher ^String path ^Trie$Matcher matcher) @@ -143,25 +146,27 @@ (if (= (get p (+ i j)) (get path j)) (recur (inc j))))))) (view [_] [path (view matcher)]) - (depth [_] (inc (depth matcher))))))) + (depth [_] (inc (depth matcher))) + (length [_] (count path)))))) -(defn wild-matcher [key matcher] - #?(:clj (Trie/wildMatcher key matcher) +(defn wild-matcher [key end matcher] + #?(:clj (Trie/wildMatcher key (if end (Character. end)) matcher) :cljs (reify Matcher (match [_ i max path] - (if (and (< i max) (not= (get path i) \/)) + (if (and (< i max) (not= (get path i) end)) (loop [percent? false, j i] (if (= max j) (if-let [match (match matcher max max path)] (-assoc! match key (decode! path i max percent?))) (let [c ^char (get path j)] - (case c - \/ (if-let [match (match matcher j max path)] - (-assoc! match key (decode! path i j percent?))) + (condp = c + end (if-let [match (match matcher j max path)] + (-assoc! match key (decode! path i j percent?))) \% (recur true (inc j)) (recur percent? (inc j)))))))) (view [_] [key (view matcher)]) - (depth [_] (inc (depth matcher)))))) + (depth [_] (inc (depth matcher))) + (length [_])))) (defn catch-all-matcher [key data] #?(:clj (Trie/catchAllMatcher key data) @@ -170,11 +175,12 @@ (match [_ i max path] (if (< i max) (-assoc! match key (decode! path i max true)))) (view [_] [key [data]]) - (depth [_] 1))))) + (depth [_] 1) + (length [_]))))) (defn linear-matcher [matchers] #?(:clj (Trie/linearMatcher matchers) - :cljs (let [matchers (vec (reverse (sort-by depth matchers))) + :cljs (let [matchers (vec (reverse (sort-by (juxt depth length) matchers))) size (count matchers)] (reify Matcher (match [_ i max path] @@ -183,7 +189,8 @@ (or (match (get matchers j) i max path) (recur (inc j)))))) (view [_] (mapv view matchers)) - (depth [_] (apply max (map depth matchers))))))) + (depth [_] (apply max 0 (map depth matchers))) + (length [_]))))) ;; ;; public api @@ -201,14 +208,17 @@ (-insert (or node (-node {})) (split-path path) data))) (defn compile [{:keys [data children wilds catch-all]}] - (let [matchers (cond-> [] - data (conj (data-matcher data)) - children (into (for [[p c] children] (static-matcher p (compile c)))) - wilds (into (for [[p c] wilds] (wild-matcher p (compile c)))) - catch-all (into (for [[p c] catch-all] (catch-all-matcher p (:data c)))))] - (if (rest matchers) - (linear-matcher matchers) - (first matchers)))) + (let [ends (fn [{:keys [children]}] (or (keys children) ["/"])) + matchers (-> [] + (cond-> data (conj (data-matcher data))) + (into (for [[p c] children] (static-matcher p (compile c)))) + (into (for [[p c] wilds, end (ends c)] + (wild-matcher (:value p) (first end) (compile (update c :children select-keys [end]))))) + (into (for [[p c] catch-all] (catch-all-matcher (:value p) (:data c)))))] + (cond + (> (count matchers) 1) (linear-matcher matchers) + (= (count matchers) 1) (first matchers) + :else (data-matcher nil)))) (defn pretty [matcher] #?(:clj (-> matcher str read-string eval) @@ -284,7 +294,13 @@ ["/v1/orgs/:org-id/topics" 57]] (insert) (compile) - (pretty)) + (pretty) + (./aprint)) + + (-> [["/{a}/2"] + ["/{a}.2"]] + (insert) + (compile)) (-> [["/kikka" 2] ["/kikka/kakka/kukka" 3] diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index f62983de..36d5d3b9 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -332,6 +332,7 @@ ;; 305ns (trie-router, no injects) ;; 281ns (trie-router, no injects, optimized) ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure + ;; 273ns (trie-router, no injects, direct-data) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -346,6 +347,7 @@ ;; 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) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench From 659aac7fd010df1324988ad5a9dd9486ab1d13b2 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 15:05:31 +0200 Subject: [PATCH 29/51] Much faster without transients --- modules/reitit-core/java-src/reitit/Trie.java | 19 ++++--------------- modules/reitit-core/src/reitit/trie.cljc | 2 +- perf-test/clj/reitit/go_perf_test.clj | 1 + .../clj/reitit/opensensors_perf_test.clj | 7 ++++++- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 738a9516..515cf039 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -3,7 +3,6 @@ package reitit; // https://www.codeproject.com/Tips/1190293/Iteration-Over-Java-Collections-with-High-Performa import clojure.lang.IPersistentMap; -import clojure.lang.ITransientMap; import clojure.lang.Keyword; import clojure.lang.PersistentArrayMap; @@ -35,26 +34,20 @@ public class Trie { case '+': hasPlus = true; break; - default: - break; } } return decode(chars, begin, end, hasPercent, hasPlus); } public static class Match { - final ITransientMap params = PersistentArrayMap.EMPTY.asTransient(); + public IPersistentMap params = PersistentArrayMap.EMPTY; public Object data; - public IPersistentMap parameters() { - return params.persistent(); - } - @Override public String toString() { Map m = new HashMap<>(); m.put(Keyword.intern("data"), data); - m.put(Keyword.intern("params"), parameters()); + m.put(Keyword.intern("params"), params); return m.toString(); } } @@ -165,8 +158,6 @@ public class Trie { @Override public Match match(int i, int max, char[] path, Match match) { if (i < max && path[i] != end) { - boolean hasPercent = false; - boolean hasPlus = false; int stop = max; for (int j = i; j < max; j++) { final char c = path[j]; @@ -174,12 +165,10 @@ public class Trie { stop = j; break; } - hasPercent = hasPercent || c == '%'; - hasPlus = hasPlus || c == '+'; } final Match m = child.match(stop, max, path, match); if (m != null) { - m.params.assoc(key, decode(path, i, stop, hasPercent, hasPlus)); + m.params = m.params.assoc(key, decode(path, i, stop)); } return m; } @@ -218,7 +207,7 @@ public class Trie { @Override public Match match(int i, int max, char[] path, Match match) { if (i < max) { - match.params.assoc(parameter, decode(path, i, max)); + match.params = match.params.assoc(parameter, decode(path, i, max)); match.data = data; return match; } diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 645c64df..c7f83269 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -226,7 +226,7 @@ (defn lookup [matcher path] #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] - (->Match (.data match) (.parameters match))) + (->Match (.data match) (.params match))) :cljs (if-let [match (match matcher 0 (count path) path)] (let [params (if-let [path-params (:path-params match)] (persistent! path-params) diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 36d5d3b9..5f49558b 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -348,6 +348,7 @@ ;; 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) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index 36e02fe0..d74a9764 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -572,6 +572,7 @@ ;; 326ns (java-segment-router) ;; 194ns (trie) ;; 160ns (trie, prioritized) + ;; 130ns (trie, non-transient, direct-data) (b! "reitit" reitit-f) ;; 2845ns @@ -585,16 +586,18 @@ ;; 373ns (trie) ;; 323ns (trie, prioritized) ;; 289ns (trie, prioritized, zero-copy) + ;; 266ns (trie, non-transient, direct-data) (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) ;; 240ns (trie, prioritized) + ;; 214ns (trie, non-transient, direct-data) (b! "reitit-ring-fast" reitit-ring-fast-f) - ;; 240ns (trie, prioritized, zero-copy) ;; 2553ns (linear-router) ;; 630ns (segment-router-backed) + ;; 464ns (trie, non-transient, direct-data) (b! "reitit-ring-linear" reitit-ring-linear-f) ;; 2137ns @@ -627,6 +630,7 @@ ;; 629ms (arraylist) ;; 409ns (transient) ;; 409ns (staticMultiMatcher) + ;; 305ns (non-persistent-params) (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))] @@ -638,6 +642,7 @@ (prof/start {}) ; "Elapsed time: 9183.657012 msecs" ; "Elapsed time: 8674.70132 msecs" + ; "Elapsed time: 6714.434915 msecs" (time (dotimes [_ 20000000] (app request))) From df38a0de05ed602b5326f747f71510f9f68d29fb Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 15:29:25 +0200 Subject: [PATCH 30/51] Test bracket syntax + fix trie conflicting rules --- modules/reitit-core/src/reitit/impl.cljc | 44 ++-------------- modules/reitit-core/src/reitit/trie.cljc | 64 +++++++++++++++++++++--- test/cljc/reitit/core_test.cljc | 51 +++++++++++++++---- 3 files changed, 102 insertions(+), 57 deletions(-) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 38727802..9f400df2 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -34,45 +34,6 @@ coll coll)) -(defn- -slice-start [[p1 :as p1s] [p2 :as p2s]] - (let [-split (fn [p] - (if-let [i (and p (str/index-of p "/"))] - [(subs p 0 i) (subs p i)] - [p])) - -slash (fn [cp p] - (cond - (not (string? cp)) [cp] - (and (string? cp) (not= (count cp) (count p))) [(subs p (count cp))] - (and (string? p) (not cp)) (-split p))) - -postcut (fn [[p :as pps]] - (let [i (and p (str/index-of p "/"))] - (if (and i (pos? i)) - (concat [(subs p 0 i) (subs p i)] (rest pps)) - pps))) - -tailcut (fn [cp [p :as ps]] (concat (-slash cp p) (rest ps)))] - (if (or (nil? p1) (nil? p2)) - [(-postcut p1s) (-postcut p2s)] - (let [cp (and (string? p1) (string? p2) (trie/common-prefix p1 p2))] - [(-tailcut cp p1s) (-tailcut cp p2s)])))) - -(defn- -slice-end [x xs] - (let [i (if (string? x) (str/index-of x "/"))] - (if (and (number? i) (pos? i)) - (concat [(subs x i)] xs) - xs))) - -(defn conflicting-routes? [route1 route2] - (loop [parts1 (-> route1 first parse :path-parts) - parts2 (-> route2 first parse :path-parts)] - (let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)] - (cond - (= s1 s2 nil) true - (or (nil? s1) (nil? s2)) false - (or (trie/catch-all? s1) (trie/catch-all? s2)) true - (or (trie/wild? s1) (trie/wild? s2)) (recur (-slice-end s1 ss1) (-slice-end s2 ss2)) - (not= s1 s2) false - :else (recur ss1 ss2))))) - (defn walk [raw-routes {:keys [path data routes expand] :or {data [], routes []} :as opts}] @@ -108,11 +69,14 @@ (cond->> (->> (walk raw-routes opts) (map-data merge-data)) coerce (into [] (keep #(coerce % opts))))) +(defn conflicting-routes? [route1 route2] + (trie/conflicting-paths? (first route1) (first route2))) + (defn path-conflicting-routes [routes] (-> (into {} (comp (map-indexed (fn [index route] [route (into #{} - (filter #(conflicting-routes? route %)) + (filter (partial conflicting-routes? route)) (subvec routes (inc index)))])) (filter (comp seq second))) routes) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index c7f83269..78d57db1 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -1,5 +1,5 @@ (ns reitit.trie - (:refer-clojure :exclude [compile -assoc!]) + (:refer-clojure :exclude [compile]) (:require [clojure.string :as str]) #?(:clj (:import [reitit Trie Trie$Match Trie$Matcher] (java.net URLDecoder)))) @@ -18,9 +18,9 @@ (depth [this]) (length [this])) -(defn -assoc! [match k v] - (let [params (or (:path-params match) (transient {}))] - (assoc match :path-params (assoc! params k v)))) +(defn assoc-path-param [match k v] + (let [params (:path-params match)] + (assoc match :path-params (assoc params k v)))) ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix (defn common-prefix [s1 s2] @@ -71,6 +71,54 @@ (defn normalize [s] (-> s (split-path) (join-path))) +;; +;; Conflict Resolution +;; + +(defn- -slice-start [[p1 :as p1s] [p2 :as p2s]] + (let [-split (fn [p] + (if-let [i (and p (str/index-of p "/"))] + [(subs p 0 i) (subs p i)] + [p])) + -slash (fn [cp p] + (cond + (not (string? cp)) [cp] + (and (string? cp) (not= (count cp) (count p))) [(subs p (count cp))] + (and (string? p) (not cp)) (-split p))) + -postcut (fn [[p :as pps]] + (let [i (and p (str/index-of p "/"))] + (if (and i (pos? i)) + (concat [(subs p 0 i) (subs p i)] (rest pps)) + pps))) + -tailcut (fn [cp [p :as ps]] (concat (-slash cp p) (rest ps)))] + (if (or (nil? p1) (nil? p2)) + [(-postcut p1s) (-postcut p2s)] + (if-let [cp (and (string? p1) (string? p2) (common-prefix p1 p2))] + [(-tailcut cp p1s) (-tailcut cp p2s)] + [p1s p2s])))) + +(defn- -slice-end [x xs] + (let [i (if (string? x) (str/index-of x "/"))] + (if (and (number? i) (pos? i)) + (concat [(subs x i)] xs) + xs))) + +(defn conflicting-paths? [path1 path2] + (loop [parts1 (split-path path1) + parts2 (split-path path2)] + (let [[[s1 & ss1] [s2 & ss2]] (-slice-start parts1 parts2)] + (cond + (= s1 s2 nil) true + (or (nil? s1) (nil? s2)) false + (or (catch-all? s1) (catch-all? s2)) true + (or (wild? s1) (wild? s2)) (recur (-slice-end s1 ss1) (-slice-end s2 ss2)) + (not= s1 s2) false + :else (recur ss1 ss2))))) + +;; +;; Creating Tries +;; + (defn- -node [m] (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) @@ -120,7 +168,7 @@ node'))) #?(:cljs - (defn decode! [path start end percent?] + (defn decode [path start end percent?] (if percent? (js/decodeURIComponent (subs path start end)) path))) (defn data-matcher [data] @@ -157,11 +205,11 @@ (loop [percent? false, j i] (if (= max j) (if-let [match (match matcher max max path)] - (-assoc! match key (decode! path i max percent?))) + (assoc-path-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! match key (decode! path i j percent?))) + (assoc-path-param match key (decode path i j percent?))) \% (recur true (inc j)) (recur percent? (inc j)))))))) (view [_] [key (view matcher)]) @@ -173,7 +221,7 @@ :cljs (let [match (->Match data nil)] (reify Matcher (match [_ i max path] - (if (< i max) (-assoc! match key (decode! path i max true)))) + (if (< i max) (assoc-path-param match key (decode path i max true)))) (view [_] [key [data]]) (depth [_] 1) (length [_]))))) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 19e061e8..75a88c56 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -79,15 +79,48 @@ ["/abba/:dabba/boo" ::boo] ["/:jabba/:dabba/:doo/:daa/*foo" ::wild]] {:router r}) - matches #(-> router (r/match-by-path %) :data :name)] - (is (= ::abba (matches "/abba"))) - (is (= ::abba2 (matches "/abba/1"))) - (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"))))) + by-path #(-> router (r/match-by-path %) :data :name)] + (is (= ::abba (by-path "/abba"))) + (is (= ::abba2 (by-path "/abba/1"))) + (is (= ::jabba2 (by-path "/abba/2"))) + (is (= ::doo (by-path "/abba/1/doo"))) + (is (= ::boo (by-path "/abba/1/boo"))) + (is (= ::baa (by-path "/abba/dabba/boo/baa"))) + (is (= ::boo (by-path "/abba/dabba/boo"))) + (is (= ::wild (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))))) + + (testing "bracket-params" + (let [router (r/router + [["/{abba}" ::abba] + ["/abba/1" ::abba2] + ["/{jabba}/2" ::jabba2] + ["/{abba}/{dabba}/doo" ::doo] + ["/abba/dabba/boo/baa" ::baa] + ["/abba/{dabba}/boo" ::boo] + ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] + ["/files/file-{name}.html" ::html] + ["/files/file-{name}.json" ::json] + ["/files/file-{name}-large.json" ::large]] + {:router r}) + by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] + (is (= [::abba {:abba "abba"}] (by-path "/abba"))) + (is (= [::abba2 {}] (by-path "/abba/1"))) + (is (= [::jabba2 {:jabba "abba"}] (by-path "/abba/2"))) + (is (= [::doo {:abba "abba", :dabba "1"}] (by-path "/abba/1/doo"))) + (is (= [::boo {:dabba "1"}] (by-path "/abba/1/boo"))) + (is (= [::baa {}] (by-path "/abba/dabba/boo/baa"))) + (is (= [::boo {:dabba "dabba"}] (by-path "/abba/dabba/boo"))) + (is (= [::wild {:a/jabba "olipa" + :a.b/dabba "kerran" + :a.b.c/doo "avaruus" + :a.b.c.d/daa "vaan" + :foo/bar "ei/toista/kertaa"}] + (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) + (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) + (is (= [::json {:name "10"}] (by-path "/files/file-10.json"))) + ;(is (= [::large {:name "10"}] (by-path "/files/file-10-large.json"))) + + )) (testing "empty path segments" (let [router (r/router From 29a54a3262fdadac5cab4d7af3b5220faf2c3d8d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 15:29:37 +0200 Subject: [PATCH 31/51] Perf-utils --- perf-test/clj/reitit/perf_utils.clj | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/perf-test/clj/reitit/perf_utils.clj b/perf-test/clj/reitit/perf_utils.clj index 0f8481be..d2427903 100644 --- a/perf-test/clj/reitit/perf_utils.clj +++ b/perf-test/clj/reitit/perf_utils.clj @@ -34,9 +34,11 @@ (mapv (fn [path] (let [request (map->Request (req path)) - time (int (* (first (:sample-mean (cc/quick-benchmark (dotimes [_ 1000] (f request)) {}))) 1e6))] - (println path "=>" time "ns") - [path time])) + results (cc/quick-benchmark (dotimes [_ 1000] (f request)) {}) + mean (int (* (first (:sample-mean results)) 1e6)) + lower (int (* (first (:lower-q results)) 1e6))] + (println path "=>" lower "/" mean "ns") + [path [mean lower]])) urls))) (defn bench [routes req no-paths?] @@ -45,8 +47,8 @@ [(str/replace path #"\:" "") name] [path name])) routes) router (reitit/router routes)] - (doseq [[path time] (bench-routes routes req #(reitit/match-by-path router %))] - (println path "\t" time)))) + (doseq [[path [mean lower]] (bench-routes routes req #(reitit/match-by-path router %))] + (println path "\t" mean lower)))) ;; ;; Perf tests @@ -58,8 +60,10 @@ (println) (suite name) (println) - (let [times (for [[path time] (bench-routes routes req f)] + (let [times (for [[path [mean lower]] (bench-routes routes req f)] (do - (when verbose? (println (format "%7s" time) "\t" path)) - time))] - (title (str "average: " (int (/ (reduce + times) (count times))))))) + (when verbose? (println (format "%7s %7s" mean lower) "\t" path)) + [mean lower]))] + (title (str "average, lower/mean: " + (int (/ (reduce + (map second times)) (count times))) "/" + (int (/ (reduce + (map first times)) (count times))))))) From ae1a8f7919c199af71ba76bb3b8f68a17995e84d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:01:06 +0200 Subject: [PATCH 32/51] Fail fast with multiple terminators. --- modules/reitit-core/src/reitit/trie.cljc | 11 +++- test/cljc/reitit/core_test.cljc | 69 +++++++++++++----------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 78d57db1..78b4ca49 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -260,8 +260,15 @@ matchers (-> [] (cond-> data (conj (data-matcher data))) (into (for [[p c] children] (static-matcher p (compile c)))) - (into (for [[p c] wilds, end (ends c)] - (wild-matcher (:value p) (first end) (compile (update c :children select-keys [end]))))) + (into + (for [[p c] wilds] + (let [p (:value p) + ends (ends c)] + (if (seq (rest ends)) + (throw + (ex-info + (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)))))] (cond (> (count matchers) 1) (linear-matcher matchers) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 75a88c56..6f266816 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -90,37 +90,46 @@ (is (= ::wild (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))))) (testing "bracket-params" - (let [router (r/router - [["/{abba}" ::abba] - ["/abba/1" ::abba2] - ["/{jabba}/2" ::jabba2] - ["/{abba}/{dabba}/doo" ::doo] - ["/abba/dabba/boo/baa" ::baa] - ["/abba/{dabba}/boo" ::boo] - ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] - ["/files/file-{name}.html" ::html] - ["/files/file-{name}.json" ::json] - ["/files/file-{name}-large.json" ::large]] - {:router r}) - by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] - (is (= [::abba {:abba "abba"}] (by-path "/abba"))) - (is (= [::abba2 {}] (by-path "/abba/1"))) - (is (= [::jabba2 {:jabba "abba"}] (by-path "/abba/2"))) - (is (= [::doo {:abba "abba", :dabba "1"}] (by-path "/abba/1/doo"))) - (is (= [::boo {:dabba "1"}] (by-path "/abba/1/boo"))) - (is (= [::baa {}] (by-path "/abba/dabba/boo/baa"))) - (is (= [::boo {:dabba "dabba"}] (by-path "/abba/dabba/boo"))) - (is (= [::wild {:a/jabba "olipa" - :a.b/dabba "kerran" - :a.b.c/doo "avaruus" - :a.b.c.d/daa "vaan" - :foo/bar "ei/toista/kertaa"}] - (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) - (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) - (is (= [::json {:name "10"}] (by-path "/files/file-10.json"))) - ;(is (= [::large {:name "10"}] (by-path "/files/file-10-large.json"))) + (testing "successful" + (let [router (r/router + [["/{abba}" ::abba] + ["/abba/1" ::abba2] + ["/{jabba}/2" ::jabba2] + ["/{abba}/{dabba}/doo" ::doo] + ["/abba/dabba/boo/baa" ::baa] + ["/abba/{dabba}/boo" ::boo] + ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] + ["/files/file-{name}.html" ::html] + ["/files/file-{name}.json" ::json]] + {:router r}) + by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] + (is (= [::abba {:abba "abba"}] (by-path "/abba"))) + (is (= [::abba2 {}] (by-path "/abba/1"))) + (is (= [::jabba2 {:jabba "abba"}] (by-path "/abba/2"))) + (is (= [::doo {:abba "abba", :dabba "1"}] (by-path "/abba/1/doo"))) + (is (= [::boo {:dabba "1"}] (by-path "/abba/1/boo"))) + (is (= [::baa {}] (by-path "/abba/dabba/boo/baa"))) + (is (= [::boo {:dabba "dabba"}] (by-path "/abba/dabba/boo"))) + (is (= [::wild {:a/jabba "olipa" + :a.b/dabba "kerran" + :a.b.c/doo "avaruus" + :a.b.c.d/daa "vaan" + :foo/bar "ei/toista/kertaa"}] + (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) + (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))))) - )) + (testing "invalid syntax fails fast" + (testing "unbalanced brackets" + (is (thrown-with-msg? + ExceptionInfo + #"^Unbalanced brackets" + (r/router ["/kikka/{kukka"])))) + (testing "multiple terminators" + (is (thrown-with-msg? + ExceptionInfo + #"^Trie compliation error: wild :kukka has two terminators" + (r/router [["/{kukka}.json"] + ["/{kukka}-json"]])))))) (testing "empty path segments" (let [router (r/router From 8abca179d0997f9448f96e5bae7b0f7bc4330711 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:04:41 +0200 Subject: [PATCH 33/51] Test unicode chars --- test/cljc/reitit/core_test.cljc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 6f266816..96a48277 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -100,7 +100,8 @@ ["/abba/{dabba}/boo" ::boo] ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] ["/files/file-{name}.html" ::html] - ["/files/file-{name}.json" ::json]] + ["/files/file-{name}.json" ::json] + ["/{eskon}/{saum}/pium\u2215paum" ::loru]] {:router r}) by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] (is (= [::abba {:abba "abba"}] (by-path "/abba"))) @@ -116,7 +117,9 @@ :a.b.c.d/daa "vaan" :foo/bar "ei/toista/kertaa"}] (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) - (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))))) + (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) + (is (= [::loru {:eskon "viitan", :saum "aa"}] (by-path "/viitan/aa/pium\u2215paum"))) + (is (= [nil nil] (by-path "/ei/osu/pium/paum"))))) (testing "invalid syntax fails fast" (testing "unbalanced brackets" From d68e1b81fbd2df26a8ff11655d71bddefcefda09 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:23:35 +0200 Subject: [PATCH 34/51] FIx cljs, welcome reitit.exception! --- modules/reitit-core/src/reitit/exception.cljc | 7 ++++++ modules/reitit-core/src/reitit/trie.cljc | 23 ++++++++----------- test/cljc/reitit/core_test.cljc | 10 ++++---- 3 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 modules/reitit-core/src/reitit/exception.cljc diff --git a/modules/reitit-core/src/reitit/exception.cljc b/modules/reitit-core/src/reitit/exception.cljc new file mode 100644 index 00000000..52470c0c --- /dev/null +++ b/modules/reitit-core/src/reitit/exception.cljc @@ -0,0 +1,7 @@ +(ns reitit.exception) + +(defn fail! + ([message] + (throw (ex-info message {:type ::exeption}))) + ([message data] + (throw (ex-info message (assoc data :type ::exeption))))) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 78b4ca49..23228689 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -1,6 +1,7 @@ (ns reitit.trie (:refer-clojure :exclude [compile]) - (:require [clojure.string :as str]) + (:require [clojure.string :as str] + [reitit.exception :as ex]) #?(:clj (:import [reitit Trie Trie$Match Trie$Matcher] (java.net URLDecoder)))) @@ -49,7 +50,7 @@ (if (= to (count s)) (concat ss (-static from to)) (case (get s to) - \{ (let [to' (or (str/index-of s "}" to) (throw (ex-info (str "Unbalanced brackets: " (pr-str s)) {})))] + \{ (let [to' (or (str/index-of s "}" to) (ex/fail! (str "Unclosed brackets: " (pr-str s))))] (if (= \* (get s (inc to))) (recur (concat ss (-static from to) (-catch-all (inc to) to')) (inc to') (inc to')) (recur (concat ss (-static from to) (-wild to to')) (inc to') (inc to')))) @@ -131,7 +132,7 @@ (instance? Wild path) (let [next (first ps)] (if (or (instance? Wild next) (instance? CatchAll next)) - (throw (ex-info (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))))) (instance? CatchAll path) @@ -169,11 +170,12 @@ #?(:cljs (defn decode [path start end percent?] - (if percent? (js/decodeURIComponent (subs path start end)) path))) + (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 nil)] + :cljs (let [match (->Match data {})] (reify Matcher (match [_ i max _] (if (= i max) @@ -264,10 +266,8 @@ (for [[p c] wilds] (let [p (:value p) ends (ends c)] - (if (seq (rest ends)) - (throw - (ex-info - (str "Trie compliation error: wild " p " has two terminators: " ends) {})) + (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)))))] (cond @@ -283,10 +283,7 @@ #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] (->Match (.data match) (.params match))) :cljs (if-let [match (match matcher 0 (count path) path)] - (let [params (if-let [path-params (:path-params match)] - (persistent! path-params) - {})] - (assoc match :path-params params))))) + (->Match (:data match) (:path-params match))))) ;; ;; spike diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 96a48277..4c80611e 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -101,7 +101,8 @@ ["/{a/jabba}/{a.b/dabba}/{a.b.c/doo}/{a.b.c.d/daa}/{*foo/bar}" ::wild] ["/files/file-{name}.html" ::html] ["/files/file-{name}.json" ::json] - ["/{eskon}/{saum}/pium\u2215paum" ::loru]] + ["/{eskon}/{saum}/pium\u2215paum" ::loru] + ["/extra-end}s-are/ok" ::bracket]] {:router r}) by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] (is (= [::abba {:abba "abba"}] (by-path "/abba"))) @@ -119,13 +120,14 @@ (by-path "/olipa/kerran/avaruus/vaan/ei/toista/kertaa"))) (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) (is (= [::loru {:eskon "viitan", :saum "aa"}] (by-path "/viitan/aa/pium\u2215paum"))) - (is (= [nil nil] (by-path "/ei/osu/pium/paum"))))) + (is (= [nil nil] (by-path "/ei/osu/pium/paum"))) + (is (= [::bracket {}] (by-path "/extra-end}s-are/ok"))))) (testing "invalid syntax fails fast" - (testing "unbalanced brackets" + (testing "unclosed brackets" (is (thrown-with-msg? ExceptionInfo - #"^Unbalanced brackets" + #"^Unclosed brackets" (r/router ["/kikka/{kukka"])))) (testing "multiple terminators" (is (thrown-with-msg? From 69550febfa6a17d85184d00a27f9ba9b81288695 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:49:23 +0200 Subject: [PATCH 35/51] dead code --- test/cljc/reitit/chain.clj | 382 ------------------------------------- 1 file changed, 382 deletions(-) delete mode 100644 test/cljc/reitit/chain.clj diff --git a/test/cljc/reitit/chain.clj b/test/cljc/reitit/chain.clj deleted file mode 100644 index ece79b96..00000000 --- a/test/cljc/reitit/chain.clj +++ /dev/null @@ -1,382 +0,0 @@ -; Copyright 2013 Relevance, Inc. -; Copyright 2014-2016 Cognitect, Inc. - -; The use and distribution terms for this software are covered by the -; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) -; which can be found in the file epl-v10.html at the root of this distribution. -; -; By using this software in any fashion, you are agreeing to be bound by -; the terms of this license. -; -; You must not remove this notice, or any other, from this software. - -(comment -(ns reitit.chain - "Interceptor pattern. Executes a chain of Interceptor functions on a - common \"context\" map, maintaining a virtual \"stack\", with error - handling and support for asynchronous execution." - (:refer-clojure :exclude (name)) - (:require [clojure.core.async :as async :refer [ex-info [^Throwable t execution-id interceptor stage] - (let [iname (name interceptor) - throwable-str (pr-str (type t))] - (ex-info (str throwable-str " in Interceptor " iname " - " (.getMessage t)) - (merge {:execution-id execution-id - :stage stage - :interceptor iname - :exception-type (keyword throwable-str) - :exception t} - (ex-data t)) - t))) - -(defn- try-f - "If f is not nil, invokes it on context. If f throws an exception, - assoc's it on to context as :error." - [context interceptor stage] - (let [execution-id (:execution-id context)] - (if-let [f (stage interceptor)] - (try (log/debug :interceptor (name interceptor) - :stage stage - :execution-id execution-id - :fn f) - (f context) - (catch Throwable t - (log/debug :throw t :execution-id execution-id) - (assoc context :error (throwable->ex-info t execution-id interceptor stage)))) - (do (log/trace :interceptor (name interceptor) - :skipped? true - :stage stage - :execution-id execution-id) - context)))) - -(defn- try-error - "If error-fn is not nil, invokes it on context and the current :error - from context." - [context interceptor] - (let [execution-id (:execution-id context)] - (if-let [error-fn (:error interceptor)] - (let [ex (:error context) - stage :error] - (log/debug :interceptor (name interceptor) - :stage :error - :execution-id execution-id) - (try (error-fn (assoc context :error nil) ex) - (catch Throwable t - (if (identical? (type t) (type (:exception ex))) - (do (log/debug :rethrow t :execution-id execution-id) - context) - (do (log/debug :throw t :suppressed (:exception-type ex) :execution-id execution-id) - (-> context - (assoc :error (throwable->ex-info t execution-id interceptor :error)) - (update :suppressed conj ex))))))) - (do (log/trace :interceptor (name interceptor) - :skipped? true - :stage :error - :execution-id execution-id) - context)))) - -(defn- check-terminators - "Invokes each predicate in :terminators on context. If any predicate - returns true, removes :queue from context." - [context] - (if (some #(% context) (:terminators context)) - (let [execution-id (:execution-id context)] - (log/debug :in 'check-terminators - :terminate? true - :execution-id execution-id) - (assoc context :queue nil)) - context)) - -(defn- prepare-for-async - "Call all of the :enter-async functions in a context. The purpose of these - functions is to ready backing servlets or any other machinery for preparing - an asynchronous response." - [{:keys [enter-async] :as context}] - (doseq [enter-async-fn enter-async] - (enter-async-fn context))) - -(defn- go-async - "When presented with a channel as the return value of an enter function, - wait for the channel to return a new-context (via a go block). When a new - context is received, restart execution of the interceptor chain with that - context. - This function is non-blocking, returning nil immediately (a signal to halt - further execution on this thread)." - ([old-context context-channel] - (prepare-for-async old-context) - (go - (if-let [new-context ( context - (assoc :queue new-queue) - (assoc :stack new-stack) - (try-f interceptor interceptor-key))] - (cond - (channel? context) (go-async (assoc old-context - :async-info {:interceptor interceptor - :stage interceptor-key - :stack new-stack}) - context) - (:error context) (assoc context :queue nil) - (not= (:bindings context) (:bindings old-context)) (assoc context :rebind true) - true (recur (check-terminators context))))))))) - -(defn- process-all - [context interceptor-key] - ;; If we're processing leave handlers, reverse the queue - (let [context (if (= interceptor-key :leave) (update context :queue reverse) context) - context (with-bindings (or (:bindings context) - {}) - (process-all-with-binding context interceptor-key))] - (if (:rebind context) - (recur (assoc context :rebind nil) interceptor-key) - context))) - -(defn- process-any-errors-with-binding - "Unwinds the context by invoking :error functions of Interceptors on - the :stack of context, but **only** if there is an :error present in the context." - [context] - (log/debug :in 'process-any-errors :execution-id (:execution-id context)) - (loop [context context] - (let [stack (:stack context)] - (log/trace :context context) - (if (empty? stack) - context - (let [interceptor (peek stack) - pre-bindings (:bindings context) - old-context context - context (assoc context :stack (pop stack)) - context (if (:error context) - (try-error context interceptor) - context)] - (cond - (channel? context) (go-async old-context context) - (not= (:bindings context) pre-bindings) (assoc context :rebind true) - true (recur context))))))) - -(defn- process-any-errors - "Establish the bindings present in `context` as thread local - bindings, and then invoke process-any-errors-with-binding. - Conditionally re-establish bindings if a change in bindings is made by an - interceptor." - [context] - (let [context (with-bindings (or (:bindings context) {}) - (process-any-errors-with-binding context))] - (if (:rebind context) - (recur (assoc context :rebind nil)) - context))) - -(defn- enter-all - "Establish the bindings present in `context` as thread local - bindings, and then invoke enter-all-with-binding. Conditionally - re-establish bindings if a change in bindings is made by an - interceptor." - [context] - (process-all context :enter)) - -(defn- leave-all-with-binding - "Unwinds the context by invoking :leave functions of Interceptors on - the :stack of context. Returns updated context." - [context] - (log/debug :in 'leave-all :execution-id (:execution-id context)) - (loop [context context] - (let [stack (:stack context)] - (log/trace :context context) - (if (empty? stack) - context - (let [interceptor (peek stack) - pre-bindings (:bindings context) - old-context context - context (assoc context :stack (pop stack)) - context (if (:error context) - (try-error context interceptor) - (try-f context interceptor :leave))] - (cond - (channel? context) (go-async old-context context) - (not= (:bindings context) pre-bindings) (assoc context :rebind true) - true (recur context))))))) - -(defn- leave-all - "Establish the bindings present in `context` as thread local - bindings, and then invoke leave-all-with-binding. Conditionally - re-establish bindings if a change in bindings is made by an - interceptor." - [context] - (let [context (with-bindings (or (:bindings context) {}) - (leave-all-with-binding context))] - (if (:rebind context) - (recur (assoc context :rebind nil)) - context))) - -(defn enqueue - "Adds interceptors to the end of context's execution queue. Creates - the queue if necessary. Returns updated context." - [context interceptors] - {:pre (every? interceptor/interceptor? interceptors)} - (log/trace :enqueue (map name interceptors) :context context) - (update context :queue - (fnil into clojure.lang.PersistentQueue/EMPTY) - interceptors)) - -(defn enqueue* - "Like 'enqueue' but vararg. - If the last argument is a sequence of interceptors, - they're unpacked and to added to the context's execution queue." - [context & interceptors-and-seq] - (if (seq? (last interceptors-and-seq)) - (enqueue context (apply list* interceptors-and-seq)) - (enqueue context interceptors-and-seq))) - -(defn terminate - "Removes all remaining interceptors from context's execution queue. - This effectively short-circuits execution of Interceptors' :enter - functions and begins executing the :leave functions." - [context] - (log/trace :in 'terminate :context context) - (assoc context :queue nil)) - -(defn terminate-when - "Adds pred as a terminating condition of the context. pred is a - function that takes a context as its argument. It will be invoked - after every Interceptor's :enter function. If pred returns logical - true, execution will stop at that Interceptor." - [context pred] - (update context :terminators conj pred)) - -(def ^:private ^AtomicLong execution-id (AtomicLong.)) - -(defn- begin [context] - (if (:execution-id context) - context - (let [execution-id (.incrementAndGet execution-id)] - (log/debug :in 'begin :execution-id execution-id) - (log/trace :context context) - (assoc context :execution-id execution-id)))) - -(defn- end [context] - (if (:execution-id context) - (do - (log/debug :in 'end :execution-id (:execution-id context) :context-keys (keys context)) - (log/trace :context context) - (assoc context :stack nil :execution-id nil)) - context)) - -(defn execute-only - "Like `execute`, but only processes the interceptors in a single direction, - using `interceptor-key` (i.e. :enter, :leave) to determine which functions - to call. - --- - Executes a queue of Interceptors attached to the context. Context - must be a map, Interceptors are added with 'enqueue'. - An Interceptor Record has keys :enter, :leave, and :error. - The value of each key is a function; missing - keys or nil values are ignored. When executing a context, all - the `interceptor-key` functions are invoked in order. As this happens, the - Interceptors are pushed on to a stack." - ([context interceptor-key] - (let [context (some-> context - map->Context - begin - (process-all interceptor-key) - terminate - process-any-errors - end)] - (if-let [ex (:error context)] - (throw ex) - context))) - ([context interceptor-key interceptors] - (execute-only (enqueue context interceptors) interceptor-key))) - -(defn execute - "Executes a queue of Interceptors attached to the context. Context - must be a map, Interceptors are added with 'enqueue'. - An Interceptor is a map or map-like object with the keys :enter, - :leave, and :error. The value of each key is a function; missing - keys or nil values are ignored. When executing a context, first all - the :enter functions are invoked in order. As this happens, the - Interceptors are pushed on to a stack. - When execution reaches the end of the queue, it begins popping - Interceptors off the stack and calling their :leave functions. - Therefore :leave functions are called in the opposite order from - :enter functions. - Both the :enter and :leave functions are called on a single - argument, the context map, and return an updated context. - If any Interceptor function throws an exception, execution stops and - begins popping Interceptors off the stack and calling their :error - functions. The :error function takes two arguments: the context and - an exception. It may either handle the exception, in which case the - execution continues with the next :leave function on the stack; or - re-throw the exception, passing control to the :error function on - the stack. If the exception reaches the end of the stack without - being handled, execute will throw it." - ([context] - (let [context (some-> context - begin - enter-all - terminate - leave-all - end)] - (if-let [ex (:error context)] - (throw ex) - context))) - ([context interceptors] - (execute (enqueue context interceptors)))) -) From ad92c437e69e318ec8a73d4f531d601c65dd4453 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:49:42 +0200 Subject: [PATCH 36/51] More dead code --- modules/reitit-core/src/reitit/coercion.cljc | 9 --------- 1 file changed, 9 deletions(-) diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 709ffb77..01144de7 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -132,15 +132,6 @@ [status (response-coercer coercion body opts)]) (into {}))) -(defn- coercers-not-compiled! [match] - (throw - (ex-info - (str - "Match didn't have a compiled coercion attached.\n" - "Maybe you should have defined a router option:\n" - "{:compile reitit.coercion/compile-request-coercers}\n") - {:match match}))) - ;; ;; api-docs ;; From 950fef88d2e0ef318aaedc5ab9fefae1060fee6b Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 16:51:34 +0200 Subject: [PATCH 37/51] Use exception/fail! in all creation-time exceptions ... to be companioned with a slick error printer --- modules/reitit-core/src/reitit/core.cljc | 22 ++++++-------- .../reitit-core/src/reitit/dependency.cljc | 16 +++++----- modules/reitit-core/src/reitit/exception.cljc | 4 +-- modules/reitit-core/src/reitit/impl.cljc | 10 +++---- .../reitit-core/src/reitit/interceptor.cljc | 26 ++++++++--------- .../reitit-core/src/reitit/middleware.cljc | 29 +++++++++---------- modules/reitit-core/src/reitit/spec.cljc | 10 +++---- modules/reitit-ring/src/reitit/ring/spec.cljc | 12 ++++---- .../reitit-spec/src/reitit/coercion/spec.cljc | 2 +- 9 files changed, 63 insertions(+), 68 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 15d6eca7..9c05ba5f 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -1,6 +1,7 @@ (ns reitit.core (:require [clojure.string :as str] [reitit.impl :as impl] + [reitit.exception :as exception] [reitit.trie :as trie])) ;; @@ -50,10 +51,7 @@ conflicts))) (defn throw-on-conflicts! [f conflicts] - (throw - (ex-info - (f conflicts) - {:conflicts conflicts}))) + (exception/fail! (f conflicts) {:conflicts conflicts})) ;; ;; Router @@ -149,11 +147,10 @@ (lookup-router compiled-routes {})) ([compiled-routes opts] (when-let [wilds (seq (filter impl/wild-route? compiled-routes))] - (throw - (ex-info - (str "can't create :lookup-router with wildcard routes: " wilds) - {:wilds wilds - :routes compiled-routes}))) + (exception/fail! + (str "can't create :lookup-router with wildcard routes: " wilds) + {:wilds wilds + :routes compiled-routes})) (let [names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] @@ -239,10 +236,9 @@ (single-static-path-router compiled-routes {})) ([compiled-routes opts] (when (or (not= (count compiled-routes) 1) (some impl/wild-route? compiled-routes)) - (throw - (ex-info - (str ":single-static-path-router requires exactly 1 static route: " compiled-routes) - {:routes compiled-routes}))) + (exception/fail! + (str ":single-static-path-router requires exactly 1 static route: " compiled-routes) + {:routes compiled-routes})) (let [[n :as names] (impl/find-names compiled-routes opts) [[p data result]] compiled-routes p #?(:clj (.intern ^String p) :cljs p) diff --git a/modules/reitit-core/src/reitit/dependency.cljc b/modules/reitit-core/src/reitit/dependency.cljc index 0a235a20..4dcc6e97 100644 --- a/modules/reitit-core/src/reitit/dependency.cljc +++ b/modules/reitit-core/src/reitit/dependency.cljc @@ -1,5 +1,6 @@ (ns reitit.dependency - "Dependency resolution for middleware/interceptors.") + "Dependency resolution for middleware/interceptors." + (:require [reitit.exception :as exception])) (defn- providers "Map from provision key to provider. `get-provides` should return the provision keys of a dependent." @@ -8,8 +9,9 @@ (into acc (map (fn [provide] (when (contains? acc provide) - (throw (ex-info (str "multiple providers for: " provide) - {::multiple-providers provide}))) + (exception/fail! + (str "multiple providers for: " provide) + {::multiple-providers provide})) [provide dependent])) (get-provides dependent))) {} nodes)) @@ -19,8 +21,9 @@ [providers k] (if (contains? providers k) (get providers k) - (throw (ex-info (str "provider missing for dependency: " k) - {::missing-provider k})))) + (exception/fail! + (str "provider missing for dependency: " k) + {::missing-provider k}))) (defn post-order "Put `nodes` in post-order. Can also be described as a reverse topological sort. @@ -37,8 +40,7 @@ (assoc colors node :grey))] [(conj nodes* node) (assoc colors node :black)]) - :grey (throw (ex-info "circular dependency" - {:cycle (drop-while #(not= % node) (conj path node))})) + :grey (exception/fail! "circular dependency" {:cycle (drop-while #(not= % node) (conj path node))}) :black [() colors])) (toposort-seq [nodes path colors] diff --git a/modules/reitit-core/src/reitit/exception.cljc b/modules/reitit-core/src/reitit/exception.cljc index 52470c0c..8b67accc 100644 --- a/modules/reitit-core/src/reitit/exception.cljc +++ b/modules/reitit-core/src/reitit/exception.cljc @@ -2,6 +2,6 @@ (defn fail! ([message] - (throw (ex-info message {:type ::exeption}))) + (throw (ex-info message {::type :exeption}))) ([message data] - (throw (ex-info message (assoc data :type ::exeption))))) + (throw (ex-info message (merge {::type ::exeption} data))))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index 9f400df2..8797b77b 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -3,7 +3,8 @@ (:require [clojure.string :as str] [clojure.set :as set] [meta-merge.core :as mm] - [reitit.trie :as trie]) + [reitit.trie :as trie] + [reitit.exception :as exception]) #?(:clj (:import (java.util.regex Pattern) (java.util HashMap Map) @@ -126,10 +127,9 @@ (when-not (every? #(contains? path-params %) required) (let [defined (-> path-params keys set) missing (set/difference required defined)] - (throw - (ex-info - (str "missing path-params for route " template " -> " missing) - {:path-params path-params, :required required}))))) + (exception/fail! + (str "missing path-params for route " template " -> " missing) + {:path-params path-params, :required required})))) (defn fast-assoc #?@(:clj [[^clojure.lang.Associative a k v] (.assoc a k v)] diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc index f95bad34..67b41510 100644 --- a/modules/reitit-core/src/reitit/interceptor.cljc +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -2,7 +2,8 @@ (:require [meta-merge.core :refer [meta-merge]] [clojure.pprint :as pprint] [reitit.core :as r] - [reitit.impl :as impl])) + [reitit.impl :as impl] + [reitit.exception :as exception])) (defprotocol IntoInterceptor (into-interceptor [this data opts])) @@ -33,10 +34,9 @@ #?(:clj clojure.lang.Keyword :cljs cljs.core.Keyword) (into-interceptor [this data {:keys [::registry] :as opts}] - (if-let [interceptor (if registry (registry this))] - (into-interceptor interceptor data opts) - (throw - (ex-info + (or (if-let [interceptor (if registry (registry this))] + (into-interceptor interceptor data opts)) + (exception/fail! (str "Interceptor " this " not found in registry.\n\n" (if (seq registry) @@ -46,16 +46,15 @@ (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) "see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n") {:id this - :registry registry})))) + :registry registry}))) #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector) (into-interceptor [[f & args :as form] data opts] (when (and (seq args) (not (fn? f))) - (throw - (ex-info - (str "Invalid Interceptor form: " form "") - {:form form}))) + (exception/fail! + (str "Invalid Interceptor form: " form "") + {:form form})) (into-interceptor (apply f args) data opts)) #?(:clj clojure.lang.Fn @@ -85,10 +84,9 @@ (let [compiled (::compiled opts 0) opts (assoc opts ::compiled (inc ^long compiled))] (when (>= ^long compiled ^long *max-compile-depth*) - (throw - (ex-info - (str "Too deep Interceptor compilation - " compiled) - {:this this, :data data, :opts opts}))) + (exception/fail! + (str "Too deep Interceptor compilation - " compiled) + {:this this, :data data, :opts opts})) (if-let [interceptor (into-interceptor (compile data opts) data opts)] (map->Interceptor (merge diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index 119f0d6d..f4eaced8 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -2,7 +2,8 @@ (:require [meta-merge.core :refer [meta-merge]] [clojure.pprint :as pprint] [reitit.core :as r] - [reitit.impl :as impl])) + [reitit.impl :as impl] + [reitit.exception :as exception])) (defprotocol IntoMiddleware (into-middleware [this data opts])) @@ -17,10 +18,9 @@ #?(:clj clojure.lang.Keyword :cljs cljs.core.Keyword) (into-middleware [this data {:keys [::registry] :as opts}] - (if-let [middleware (if registry (registry this))] - (into-middleware middleware data opts) - (throw - (ex-info + (or (if-let [middleware (if registry (registry this))] + (into-middleware middleware data opts)) + (exception/fail! (str "Middleware " this " not found in registry.\n\n" (if (seq registry) @@ -30,7 +30,7 @@ (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) "see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n") {:id this - :registry registry})))) + :registry registry}))) #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector) @@ -61,10 +61,9 @@ (let [compiled (::compiled opts 0) opts (assoc opts ::compiled (inc ^long compiled))] (when (>= ^long compiled ^long *max-compile-depth*) - (throw - (ex-info - (str "Too deep Middleware compilation - " compiled) - {:this this, :data data, :opts opts}))) + (exception/fail! + (str "Too deep Middleware compilation - " compiled) + {:this this, :data data, :opts opts})) (if-let [middeware (into-middleware (compile data opts) data opts)] (map->Middleware (merge @@ -76,11 +75,11 @@ (defn- ensure-handler! [path data scope] (when-not (:handler data) - (throw (ex-info - (str "path \"" path "\" doesn't have a :handler defined" - (if scope (str " for " scope))) - (merge {:path path, :data data} - (if scope {:scope scope})))))) + (exception/fail! + (str "path \"" path "\" doesn't have a :handler defined" + (if scope (str " for " scope))) + (merge {:path path, :data data} + (if scope {:scope scope}))))) (defn- expand-and-transform [middleware data {:keys [::transform] :or {transform identity} :as opts}] diff --git a/modules/reitit-core/src/reitit/spec.cljc b/modules/reitit-core/src/reitit/spec.cljc index 91ba9a6d..0c3e2e36 100644 --- a/modules/reitit-core/src/reitit/spec.cljc +++ b/modules/reitit-core/src/reitit/spec.cljc @@ -1,7 +1,8 @@ (ns reitit.spec (:require [clojure.spec.alpha :as s] [clojure.spec.gen.alpha :as gen] - [reitit.core :as reitit])) + [reitit.core :as reitit] + [reitit.exception :as exception])) ;; ;; routes @@ -119,10 +120,9 @@ problems))) (defn throw-on-problems! [problems explain] - (throw - (ex-info - (problems-str problems explain) - {:problems problems}))) + (exception/fail! + (problems-str problems explain) + {:problems problems})) (defn validate-route-data [routes spec] (->> (for [[p d _] routes] diff --git a/modules/reitit-ring/src/reitit/ring/spec.cljc b/modules/reitit-ring/src/reitit/ring/spec.cljc index a4b6b89f..7bc4d459 100644 --- a/modules/reitit-ring/src/reitit/ring/spec.cljc +++ b/modules/reitit-ring/src/reitit/ring/spec.cljc @@ -1,7 +1,8 @@ (ns reitit.ring.spec (:require [clojure.spec.alpha :as s] [reitit.middleware :as middleware] - [reitit.spec :as rs])) + [reitit.spec :as rs] + [reitit.exception :as exception])) ;; ;; Specs @@ -19,11 +20,10 @@ (defn merge-specs [specs] (when-let [non-specs (seq (remove #(or (s/spec? %) (s/get-spec %)) specs))] - (throw - (ex-info - (str "Not all specs satisfy the Spec protocol: " non-specs) - {:specs specs - :non-specs non-specs}))) + (exception/fail! + (str "Not all specs satisfy the Spec protocol: " non-specs) + {:specs specs + :non-specs non-specs})) (s/merge-spec-impl (vec specs) (vec specs) nil)) (defn validate-route-data [routes key spec] diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 44cf495a..72086a61 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -114,7 +114,7 @@ (throw (ex-info (str "Can't produce Spec apidocs for " spesification) - {:type spesification, :coercion :spec})))) + {:spesification spesification, :coercion :spec})))) (-compile-model [_ model name] (into-spec model name)) (-open-model [_ spec] spec) From 9422cd28c1f32411e094f56adbaec419955416e1 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 19:47:41 +0200 Subject: [PATCH 38/51] create empty path-parameters at creation time, 266ns->251ns (-6%) --- doc/images/opensensors.png | Bin 31497 -> 30778 bytes modules/reitit-core/java-src/reitit/Trie.java | 56 ++++++++------- modules/reitit-core/src/reitit/core.cljc | 4 +- modules/reitit-core/src/reitit/trie.cljc | 64 +++++++++--------- perf-test/clj/reitit/go_perf_test.clj | 6 +- .../clj/reitit/opensensors_perf_test.clj | 5 ++ perf-test/clj/reitit/perf_utils.clj | 2 +- test/cljc/reitit/trie_test.cljc | 8 +-- 8 files changed, 80 insertions(+), 65 deletions(-) diff --git a/doc/images/opensensors.png b/doc/images/opensensors.png index 9f65dff4a2b6560d5b78c573fda9e9107faf59b5..fd922808558bb073a55058c0e8f8bc1a29dbfc0f 100644 GIT binary patch delta 27293 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 b@F}J%y^YRk2{ m = new HashMap<>(); @@ -53,7 +58,7 @@ public class Trie { } public interface Matcher { - Match match(int i, int max, char[] path, Match match); + Match match(int i, int max, char[] path); int depth(); @@ -76,7 +81,7 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (max < i + size) { return null; } @@ -85,7 +90,7 @@ public class Trie { return null; } } - return child.match(i + size, max, path, match); + return child.match(i + size, max, path); } @Override @@ -104,22 +109,23 @@ public class Trie { } } - public static DataMatcher dataMatcher(Object data) { - return new DataMatcher(data); + public static DataMatcher dataMatcher(IPersistentMap params, Object data) { + return new DataMatcher(params, data); } static final class DataMatcher implements Matcher { + private final IPersistentMap params; private final Object data; - DataMatcher(Object data) { + DataMatcher(IPersistentMap params, Object data) { + this.params = params; this.data = data; } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i == max) { - match.data = data; - return match; + return new Match(params, data); } return null; } @@ -156,7 +162,7 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i < max && path[i] != end) { int stop = max; for (int j = i; j < max; j++) { @@ -166,7 +172,7 @@ public class Trie { break; } } - final Match m = child.match(stop, max, path, match); + final Match m = child.match(stop, max, path); if (m != null) { m.params = m.params.assoc(key, decode(path, i, stop)); } @@ -191,25 +197,25 @@ public class Trie { } } - public static CatchAllMatcher catchAllMatcher(Keyword parameter, Object data) { - return new CatchAllMatcher(parameter, data); + public static CatchAllMatcher catchAllMatcher(Keyword parameter, IPersistentMap params, Object data) { + return new CatchAllMatcher(parameter, params, data); } static final class CatchAllMatcher implements Matcher { private final Keyword parameter; + private final IPersistentMap params; private final Object data; - CatchAllMatcher(Keyword parameter, Object data) { + CatchAllMatcher(Keyword parameter, IPersistentMap params, Object data) { this.parameter = parameter; + this.params = params; this.data = data; } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { if (i < max) { - match.params = match.params.assoc(parameter, decode(path, i, max)); - match.data = data; - return match; + return new Match(params.assoc(parameter, decode(path, i, max)), data); } return null; } @@ -226,7 +232,7 @@ public class Trie { @Override public String toString() { - return "[" + parameter + " " + new DataMatcher(data) + "]"; + return "[" + parameter + " " + new DataMatcher(null, data) + "]"; } } @@ -246,9 +252,9 @@ public class Trie { } @Override - public Match match(int i, int max, char[] path, Match match) { + public Match match(int i, int max, char[] path) { for (int j = 0; j < size; j++) { - final Match m = childs[j].match(i, max, path, match); + final Match m = childs[j].match(i, max, path); if (m != null) { return m; } @@ -273,7 +279,7 @@ public class Trie { } public static Object lookup(Matcher matcher, String path) { - return matcher.match(0, path.length(), path.toCharArray(), new Match()); + return matcher.match(0, path.length(), path.toCharArray()); } public static void main(String[] args) { @@ -283,8 +289,8 @@ public class Trie { staticMatcher("/auth/", linearMatcher( Arrays.asList( - staticMatcher("login", dataMatcher(1)), - staticMatcher("recovery", dataMatcher(2))))))); + staticMatcher("login", dataMatcher(null, 1)), + staticMatcher("recovery", dataMatcher(null, 2))))))); System.err.println(matcher); System.out.println(lookup(matcher, "/auth/login")); System.out.println(lookup(matcher, "/auth/recovery")); diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 9c05ba5f..fbab995f 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -131,7 +131,7 @@ (match-by-path [_ path] (if-let [match (trie/lookup matcher path)] (-> (:data match) - (assoc :path-params (:path-params match)) + (assoc :path-params (:params match)) (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] @@ -220,7 +220,7 @@ (match-by-path [_ path] (if-let [match (trie/lookup pl path)] (-> (:data match) - (assoc :path-params (:path-params match)) + (assoc :path-params (:params match)) (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 23228689..cd04da2e 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -7,8 +7,8 @@ (defrecord Wild [value]) (defrecord CatchAll [value]) -(defrecord Match [data path-params]) -(defrecord Node [children wilds catch-all data]) +(defrecord Match [params data]) +(defrecord Node [children wilds catch-all params data]) (defn wild? [x] (instance? Wild x)) (defn catch-all? [x] (instance? CatchAll x)) @@ -19,9 +19,9 @@ (depth [this]) (length [this])) -(defn assoc-path-param [match k v] - (let [params (:path-params match)] - (assoc match :path-params (assoc params k v)))) +(defn assoc-param [match k v] + (let [params (:params match)] + (assoc match :params (assoc params k v)))) ;; https://stackoverflow.com/questions/8033655/find-longest-common-prefix (defn common-prefix [s1 s2] @@ -121,25 +121,25 @@ ;; (defn- -node [m] - (map->Node (merge {:children {}, :wilds {}, :catch-all {}} m))) + (map->Node (merge {:children {}, :wilds {}, :catch-all {}, :params {}} m))) -(defn- -insert [node [path & ps] data] +(defn- -insert [node [path & ps] params data] (let [node' (cond (nil? path) - (assoc node :data data) + (assoc node :data data :params params) (instance? Wild path) (let [next (first ps)] (if (or (instance? Wild next) (instance? CatchAll next)) (ex/fail! (str "Two following wilds: " path ", " next)) - (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps data))))) + (update-in node [:wilds path] (fn [n] (-insert (or n (-node {})) ps params data))))) (instance? CatchAll path) - (assoc-in node [:catch-all path] (-node {:data data})) + (assoc-in node [:catch-all path] (-node {:params params, :data data})) (str/blank? path) - (-insert node ps data) + (-insert node ps params data) :else (or @@ -148,20 +148,20 @@ (if-let [cp (common-prefix p path)] (if (= cp p) ;; insert into child node - (let [n' (-insert n (conj ps (subs path (count p))) data)] + (let [n' (-insert n (conj ps (subs path (count p))) params data)] (reduced (assoc-in node [:children p] n'))) ;; split child node (let [rp (subs p (count cp)) rp' (subs path (count cp)) - n' (-insert (-node {}) ps data) - n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil)] + n' (-insert (-node {}) ps params data) + n'' (-insert (-node {:children {rp n, rp' n'}}) nil nil nil)] (reduced (update node :children (fn [children] (-> children (dissoc p) (assoc cp n''))))))))) nil (:children node)) ;; new child node - (assoc-in node [:children path] (-insert (-node {}) ps data))))] + (assoc-in node [:children path] (-insert (-node {}) ps params data))))] (if-let [child (get-in node' [:children ""])] ;; optimize by removing empty paths (-> (merge-with merge (dissoc node' :data) child) @@ -173,9 +173,9 @@ (let [param (subs path start end)] (if percent? (js/decodeURIComponent param) param)))) -(defn data-matcher [data] - #?(:clj (Trie/dataMatcher data) - :cljs (let [match (->Match data {})] +(defn data-matcher [params data] + #?(:clj (Trie/dataMatcher params data) + :cljs (let [match (->Match params data)] (reify Matcher (match [_ i max _] (if (= i max) @@ -207,23 +207,23 @@ (loop [percent? false, j i] (if (= max j) (if-let [match (match matcher max max path)] - (assoc-path-param match key (decode path i max percent?))) + (assoc-param match key (decode path i max percent?))) (let [c ^char (get path j)] (condp = c end (if-let [match (match matcher j max path)] - (assoc-path-param match key (decode path i j percent?))) + (assoc-param match key (decode path i j percent?))) \% (recur true (inc j)) (recur percent? (inc j)))))))) (view [_] [key (view matcher)]) (depth [_] (inc (depth matcher))) (length [_])))) -(defn catch-all-matcher [key data] - #?(:clj (Trie/catchAllMatcher key data) - :cljs (let [match (->Match data nil)] +(defn catch-all-matcher [key params data] + #?(:clj (Trie/catchAllMatcher key params data) + :cljs (let [match (->Match params data)] (reify Matcher (match [_ i max path] - (if (< i max) (assoc-path-param match key (decode path i max true)))) + (if (< i max) (assoc-param match key (decode path i max true)))) (view [_] [key [data]]) (depth [_] 1) (length [_]))))) @@ -255,12 +255,14 @@ (insert acc p d)) node routes)) ([node path data] - (-insert (or node (-node {})) (split-path path) data))) + (let [parts (split-path path) + params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] + (-insert (or node (-node {})) (split-path path) params data)))) -(defn compile [{:keys [data children wilds catch-all]}] +(defn compile [{:keys [data params children wilds catch-all] :or {params {}}}] (let [ends (fn [{:keys [children]}] (or (keys children) ["/"])) matchers (-> [] - (cond-> data (conj (data-matcher data))) + (cond-> data (conj (data-matcher params data))) (into (for [[p c] children] (static-matcher p (compile c)))) (into (for [[p c] wilds] @@ -269,11 +271,11 @@ (if (next ends) (ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends)) (wild-matcher p (ffirst ends) (compile c)))))) - (into (for [[p c] catch-all] (catch-all-matcher (:value p) (:data c)))))] + (into (for [[p c] catch-all] (catch-all-matcher (:value p) params (:data c)))))] (cond (> (count matchers) 1) (linear-matcher matchers) (= (count matchers) 1) (first matchers) - :else (data-matcher nil)))) + :else (data-matcher {} nil)))) (defn pretty [matcher] #?(:clj (-> matcher str read-string eval) @@ -281,9 +283,9 @@ (defn lookup [matcher path] #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] - (->Match (.data match) (.params match))) + (->Match (.params match) (.data match))) :cljs (if-let [match (match matcher 0 (count path) path)] - (->Match (:data match) (:path-params match))))) + (->Match (:params match) (:data match))))) ;; ;; spike diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 5f49558b..59838449 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -333,6 +333,7 @@ ;; 281ns (trie-router, no injects, optimized) ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 273ns (trie-router, no injects, direct-data) + ;; 256ns (trie-router, pre-defined parameters) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -347,8 +348,9 @@ ;; 66µs (trie-router, no injects) ;; 64µs (trie-router, no injects, optimized) - 124µs (clojure) ;; 63µs (trie-router, no injects, switch-case) - 124µs (clojure) - ;; 63ns (trie-router, no injects, direct-data) - ;; 54ns (trie-router, non-transient params) + ;; 63µs (trie-router, no injects, direct-data) + ;; 54µs (trie-router, non-transient params) + ;; 49µs (trie-router, pre-defined parameters) (let [requests (mapv route->req routes)] (title "all") (cc/quick-bench diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index d74a9764..c00a89a6 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -573,6 +573,7 @@ ;; 194ns (trie) ;; 160ns (trie, prioritized) ;; 130ns (trie, non-transient, direct-data) + ;; 121ns (trie, pre-defined parameters) (b! "reitit" reitit-f) ;; 2845ns @@ -587,12 +588,14 @@ ;; 323ns (trie, prioritized) ;; 289ns (trie, prioritized, zero-copy) ;; 266ns (trie, non-transient, direct-data) + ;; 251ns (trie, pre-defined parameters) (b! "reitit-ring" reitit-ring-f) ;; 385ns (java-segment-router, no injects) ;; 271ms (trie) ;; 240ns (trie, prioritized) ;; 214ns (trie, non-transient, direct-data) + ;; 187ns (trie, pre-defined parameters) (b! "reitit-ring-fast" reitit-ring-fast-f) ;; 2553ns (linear-router) @@ -631,6 +634,7 @@ ;; 409ns (transient) ;; 409ns (staticMultiMatcher) ;; 305ns (non-persistent-params) + ;; 293ns (pre-defined parameters) (let [app (ring/ring-handler (ring/router opensensors-routes) {:inject-match? false, :inject-router? false}) request {:uri "/v1/users/1/devices/1", :request-method :get}] (doseq [[p r] (-> app (ring/get-router) (r/routes))] @@ -643,6 +647,7 @@ ; "Elapsed time: 9183.657012 msecs" ; "Elapsed time: 8674.70132 msecs" ; "Elapsed time: 6714.434915 msecs" + ; "Elapsed time: 6325.310043 msecs" (time (dotimes [_ 20000000] (app request))) diff --git a/perf-test/clj/reitit/perf_utils.clj b/perf-test/clj/reitit/perf_utils.clj index d2427903..49d0c485 100644 --- a/perf-test/clj/reitit/perf_utils.clj +++ b/perf-test/clj/reitit/perf_utils.clj @@ -62,7 +62,7 @@ (println) (let [times (for [[path [mean lower]] (bench-routes routes req f)] (do - (when verbose? (println (format "%7s %7s" mean lower) "\t" path)) + (when verbose? (println (format "%7s\t%7s" mean lower) "\t" path)) [mean lower]))] (title (str "average, lower/mean: " (int (/ (reduce + (map second times)) (count times))) "/" diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index b56981dd..291937c5 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -14,24 +14,24 @@ "/olipa/kerran/{*valvavan.suuri/avaruus}", "/olipa/kerran/{*valvavan.suuri/avaruus}")) (deftest tests - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/compile) (trie/lookup "/foo")))) - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) (trie/lookup "/foo")))) - (is (= (trie/->Match {:b 1} {:bar "bar"}) + (is (= (trie/->Match {:bar "bar"} {:b 1}) (-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) (trie/lookup "/foo/bar")))) - (is (= (trie/->Match {:a 1} {}) + (is (= (trie/->Match {} {:a 1}) (-> (trie/insert nil "" {:a 1}) (trie/compile) (trie/lookup ""))))) From dc2519238a02b1fcc719dce173b6bb4d605c0ffc Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 20:06:32 +0200 Subject: [PATCH 39/51] More perf tests --- doc/performance.md | 17 +++++++++++------ perf-test/clj/reitit/bide_perf_test.clj | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/performance.md b/doc/performance.md index 76ef31c9..3f3df2c5 100644 --- a/doc/performance.md +++ b/doc/performance.md @@ -1,6 +1,6 @@ # Performance -Besides having great features, goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten. +Reitit tries to be really, really fast. ![Opensensors perf test](images/opensensors.png) @@ -9,9 +9,10 @@ Besides having great features, goal of reitit is to be really, really fast. The * Multiple routing algorithms, chosen based on the route tree * Route flattening and re-ordering * Managed mutability over immutability -* Precompute/compile as much as possible (matches, middleware, interceptors, routes) +* Precompute/compile as much as possible (matches, middleware, interceptors, routes, path-parameter sets) * Use abstractions that enable JVM optimizations * Use small functions to enable JVM Inlining +* Use Java where needed * Protocols over Multimethods * Records over Maps * Always be measuring @@ -63,13 +64,13 @@ The routing sample taken from [bide](https://github.com/funcool/bide) README: (dotimes [_ 1000] (r/match-by-path routes "/auth/login"))) -;; Execution time mean (per 1000): 315 µs -> 3.2M ops/sec +;; Execution time mean (per 1000): 115 µs -> 8.7M 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 6-40x 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 18-110x 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 +80,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 [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 [Wildcard Trie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/Trie.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) @@ -93,13 +94,17 @@ Both `reitit-ring` and Pedestal shine in this test, thanks to the fast lookup-ro **NOTE**: in real life, there are usually always also wild-card routes present. In this case, Pedestal would fallback from lookup-router to the prefix-tree router, which is order of magnitude slower (30x in this test). Reitit would handle this nicely thanks to it's `:mixed-router`: all static routes would still be served with `:lookup-router`, just the wildcard routes with `:segment-tree`. The performance would not notably degrade. +### Path conflicts + +**TODO** + ### Why measure? The reitit routing perf is measured to get an internal baseline to optimize against. We also want to ensure that new features don't regress the performance. Perf tests should be run in a stable CI environment. Help welcome! ### 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 kinda awesome. +A quick poke to [the fast routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that reitit is less 50% slower than the fastest routers in Go. Which is kinda awesome. ### Faster! diff --git a/perf-test/clj/reitit/bide_perf_test.clj b/perf-test/clj/reitit/bide_perf_test.clj index 885a32bc..fa05247e 100644 --- a/perf-test/clj/reitit/bide_perf_test.clj +++ b/perf-test/clj/reitit/bide_perf_test.clj @@ -165,6 +165,7 @@ ;; 530 µs (4-24x) -25% prefix-tree-router ;; 710 µs (3-18x) segment-router ;; 320 µs (6-40x) java-segment-router + ;; 115 µs (18-111x) trie-router (title "reitit") (assert (reitit/match-by-path reitit-routes "/workspace/1/1")) (cc/quick-bench From 277faf48f5da4ca355417094c431eb0f3b09c381 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 Feb 2019 21:18:02 +0200 Subject: [PATCH 40/51] Server benchmarks --- perf-test/clj/reitit/json_perf.cljc | 47 +++++++++++++++++------ perf-test/clj/reitit/nodejs_perf_test.clj | 16 ++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/perf-test/clj/reitit/json_perf.cljc b/perf-test/clj/reitit/json_perf.cljc index b9d81dfd..5c26dd0a 100644 --- a/perf-test/clj/reitit/json_perf.cljc +++ b/perf-test/clj/reitit/json_perf.cljc @@ -2,9 +2,13 @@ (:require [criterium.core :as cc] [reitit.perf-utils :refer :all] + ;; aleph + [aleph.http :as http] + ;; reitit [reitit.ring :as ring] - [muuntaja.middleware :as mm] + [muuntaja.core :as m] + [reitit.ring.middleware.muuntaja :as rm] ;; bidi-yada [yada.yada :as yada] @@ -14,7 +18,8 @@ ;; defaults [ring.middleware.defaults :as defaults] [compojure.core :as compojure] - [clojure.string :as str])) + [clojure.string :as str] + [muuntaja.middleware :as mm])) ;; ;; start repl with `lein perf repl` @@ -31,16 +36,16 @@ ;; Memory: 16 GB ;; -;; TODO: naive implementation (defn- with-security-headers [response] - (update + (assoc response :headers - (fn [headers] - (-> headers - (assoc "x-frame-options" "SAMEORIGIN") - (assoc "x-xss-protection" "1; mode=block") - (assoc "x-content-type-options" "nosniff"))))) + (reduce-kv + assoc + {"x-frame-options" "SAMEORIGIN" + "x-xss-protection" "1; mode=block" + "x-content-type-options" "nosniff"} + (:headers response)))) (def security-middleware {:name ::security @@ -53,7 +58,8 @@ (ring/router ["/api/ping" {:get {:handler (fn [_] {:status 200, :body {:ping "pong"}})}}] - {:data {:middleware [mm/wrap-format + {:data {:muuntaja (m/create (assoc m/default-options :return :bytes)) + :middleware [rm/format-middleware security-middleware]}}))) (def bidi-yada-app @@ -87,7 +93,7 @@ (defn perf-test [] - ;; 176µs + ;; 206µs (title "compojure + ring-defaults") (let [f (fn [] (defaults-app request))] (expect! (-> (f) :body slurp)) @@ -99,7 +105,7 @@ (expect! (-> (f) deref :body bs/to-string)) (cc/quick-bench (f))) - ;; 5.0µs + ;; 6.0µs (title "reitit-ring") (let [f (fn [] (reitit-app request))] (expect! (-> (f) :body slurp)) @@ -107,3 +113,20 @@ (comment (perf-test)) + +(comment + + ;; 10198 + ;; http :3000/api/ping + ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3000/api/ping + (http/start-server defaults-app {:port 3000}) + + ;; 16230 + ;; http :3001/api/ping + ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3001/api/ping + (http/start-server bidi-yada-app {:port 3001}) + + ;; 48084 + ;; http :3002/api/ping + ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:3002/api/ping + (http/start-server reitit-app {:port 3002})) diff --git a/perf-test/clj/reitit/nodejs_perf_test.clj b/perf-test/clj/reitit/nodejs_perf_test.clj index 7b30f118..5a427e1c 100644 --- a/perf-test/clj/reitit/nodejs_perf_test.clj +++ b/perf-test/clj/reitit/nodejs_perf_test.clj @@ -29,6 +29,9 @@ (for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]] [(str "/" name "/:id") {:get (partial h name)}])))) +(for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]] + [(str "/" name "/:id") {:get (partial h name)}]) + (app {:request-method :get, :uri "/product/foo"}) (defn routing-test [] @@ -79,3 +82,16 @@ (comment (web/run app {:port 2048, :dispatch? false, :server {:always-set-keep-alive false}}) (routing-test)) + +(comment + (require '[compojure.core :as c]) + (def app (apply + c/routes + (for [name ["product" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "twenty"]] + (eval `(c/GET ~(str "/" name "/:id") [~'id] (str "Got " ~name " id " ~'id)))))) + + (require '[ring.adapter.jetty :as jetty]) + ;; 57862 / 54290 + ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:8080/product/foo + ;; wrk -d ${DURATION:="30s"} http://127.0.0.1:8080/twenty/bar + (jetty/run-jetty app {:port 8080})) From e60d176a2d0e051b0b75e93a67a64216f9622ed0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 10 Feb 2019 12:42:32 +0200 Subject: [PATCH 41/51] Validate after route conflicts --- modules/reitit-core/src/reitit/core.cljc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index fbab995f..7d89aba1 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -343,11 +343,11 @@ ;; (defn ^:no-doc default-router-options [] - {:lookup (fn [[_ {:keys [name]}] _] (if name #{name})) + {:lookup (fn lookup [[_ {:keys [name]}] _] (if name #{name})) :expand expand - :coerce (fn [route _] route) - :compile (fn [[_ {:keys [handler]}] _] handler) - :conflicts (partial throw-on-conflicts! path-conflicts-str)}) + :coerce (fn coerce [route _] route) + :compile (fn compile [[_ {:keys [handler]}] _] handler) + :conflicts (fn throw! [conflicts] (throw-on-conflicts! path-conflicts-str conflicts))}) (defn router "Create a [[Router]] from raw route data and optionally an options map. @@ -384,13 +384,13 @@ all-wilds? trie-router :else mixed-router)] - (when-let [validate (:validate opts)] - (validate compiled-routes opts)) - (when-let [conflicts (:conflicts opts)] (when path-conflicting (conflicts path-conflicting))) (when name-conflicting (throw-on-conflicts! name-conflicts-str name-conflicting)) + (when-let [validate (:validate opts)] + (validate compiled-routes opts)) + (router compiled-routes opts)))) From 885ca8813cdb2e8d044ba277de6888fcc5483021 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 10 Feb 2019 12:43:00 +0200 Subject: [PATCH 42/51] Update docs, including brackets --- doc/basics/route_data.md | 92 +++++++++++++++++++++++++++++++++++--- doc/basics/route_syntax.md | 54 +++++++++++++++++++--- doc/basics/router.md | 72 +++++++++++++++++++++++++++-- 3 files changed, 200 insertions(+), 18 deletions(-) diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md index 212b5149..b615ab7c 100644 --- a/doc/basics/route_data.md +++ b/doc/basics/route_data.md @@ -1,8 +1,17 @@ # Route Data -Route data is the core feature of reitit. Routes can have any map-like data attached to them. This data is interpreted either by the client application or the `Router` via its `:coerce` and `:compile` hooks. Route data format can be defined and validated with `clojure.spec` enabling an architecture of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. +Route data is the key feature of reitit. Routes can have any map-like data attached to them, to be interpreted by the client application, `Router` or routing components like `Middleware` or `Interceptors`. -Raw routes can have a non-sequential route argument that is expanded (via router `:expand` hook) into route data at router creation time. By default, Keywords are expanded into `:name` and functions into `:handler` keys. +```clj +[["/ping" {:name ::ping}] + ["/pong" {:handler identity}] + ["/users" {:get {:roles #{:admin} + :handler identity}}]] +``` + +Besides map-like data, raw routes can have any non-sequential route argument after the path. This argument is expanded by `Router` (via `:expand` option) into route data at router creation time. + +By default, Keywords are expanded into `:name` and functions into `:handler` keys. ```clj (require '[reitit.core :as r]) @@ -15,11 +24,13 @@ Raw routes can have a non-sequential route argument that is expanded (via router :handler identity}}]])) ``` -The expanded route data can be retrieved from a router with `routes` and is returned with `match-by-path` and `match-by-name` in case of a route match. +## Using Route Data + +Expanded route data can be retrieved from a router with `routes` and is returned with `match-by-path` and `match-by-name` in case of a route match. ```clj (r/routes router) -; [["/ping" {:name :user/ping}] +; [["/ping" {:name ::ping}] ; ["/pong" {:handler identity]} ; ["/users" {:get {:roles #{:admin} ; :handler identity}}]] @@ -43,7 +54,7 @@ The expanded route data can be retrieved from a router with `routes` and is retu ; :path "/ping"} ``` -## Nested route data +## Nested Route Data For nested route trees, route data is accumulated recursively from root towards leafs using [meta-merge](https://github.com/weavejester/meta-merge). Default behavior for collections is `:append`, but this can be overridden to `:prepend`, `:replace` or `:displace` using the target meta-data. @@ -73,9 +84,72 @@ Resolved route tree: ; :roles #{:db-admin}}]] ``` +## Route Data Fragments + +Just like [fragments in React.js](https://reactjs.org/docs/fragments.html), we can create routing tree fragments by using empty path `""`. This allows us to add route data without accumulating to path. + +Given a route tree: + +```clj +[["/swagger.json" ::swagger] + ["/api-docs" ::api-docs] + ["/api/ping" ::ping] + ["/api/pong" ::pong]] +``` + +Adding `:no-doc` route data to exclude the first routes from generated [Swagger documentation](../ring/swagger.md): + +```clj +[["" {:no-doc true} + ["/swagger.json" ::swagger] + ["/api-docs" ::api-docs]] + ["/api/ping" ::ping] + ["/api/pong" ::pong]] +``` + +Accumulated route data: + +```clj +(def router + (r/router + [["" {:no-doc true} + ["/swagger.json" ::swagger] + ["/api-docs" ::api-docs]] + ["/api/ping" ::ping] + ["/api/pong" ::pong]])) + +(r/routes router) +; [["/swagger.json" {:no-doc true, :name ::swagger}] +; ["/api-docs" {:no-doc true, :name ::api-docs}] +; ["/api/ping" {:name ::ping}] +; ["/api/pong" {:name ::pong}]] +``` + +## Top-level Route Data + +Route data can be introduced also via `Router` option `:data`: + +```clj +(def router + (r/router + ["/api" + {:middleware [::api]} + ["/ping" ::ping] + ["/pong" ::pong]] + {:data {:middleware [::session]}})) +``` + +Expanded routes: + +```clj +[["/api/ping" {:middleware [::session ::api], :name ::ping}] + ["/api/pong" {:middleware [::session ::api], :name ::pong}]] +``` + + ## Expansion -By default, router `:expand` hook maps to `reitit.core/expand` function, backed by a `reitit.core/Expand` protocol. One can provide either a totally different function or add new implementations to that protocol. Expand implementations can be recursive. +By default, router `:expand` option has value `r/expand` function, backed by a `r/Expand` protocol. Expansion can be customized either by swapping the `:expand` implementation or by extending the Protocol. `r/Expand` implementations can be recursive. Naive example to add direct support for `java.io.File` route argument: @@ -91,4 +165,8 @@ Naive example to add direct support for `java.io.File` route argument: ["/" (java.io.File. "index.html")]) ``` -See [router options](../advanced/configuring_routers.md) for all available options. +Page [shared routes](../advanced/shared_routes.md#using-custom-expander) has an example of an custom `:expand` implementation. + +## Route data validation + +See [Route data validation](route_data_validation.md), which enables an architecture of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. diff --git a/doc/basics/route_syntax.md b/doc/basics/route_syntax.md index b96a8501..cb3216a2 100644 --- a/doc/basics/route_syntax.md +++ b/doc/basics/route_syntax.md @@ -4,7 +4,7 @@ Routes are defined as vectors of String path and optional (non-sequential) route Routes can be wrapped in vectors and lists and `nil` routes are ignored. -Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). +Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since `0.4.0`, parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. The non-bracket syntax might be deprecated later. ### Examples @@ -35,12 +35,21 @@ Routes with path parameters: ["/api/:version/ping"]] ``` +```clj +[["/users/{user-id}"] + ["/files/file-{number}.pdf"]] +``` + Route with catch-all parameter: ```clj ["/public/*path"] ``` +```clj +["/public/{*path}"] +``` + Nested routes: ```clj @@ -59,6 +68,42 @@ Same routes flattened: ["/api/ping" {:name ::ping}]] ``` +### Encoding + +Reitit does not apply any encoding to your paths. If you need that, you must encode them yourself. E.g., `/foo bar` should be `/foo%20bar`. + +### Wildcards + +Normal path-parameters (`:id`) can start anywhere in the path string, but have to end either to slash `/` (currently hardcoded) or to en end of path string: + +```clj +[["/api/:version"] + ["/files/file-:number"] + ["/user/:user-id/orders"]] +``` + +Bracket path-parameters can start and stop anywhere in the path-string, the following character is used as a terminator. + +```clj +[["/api/{version}"] + ["/files/{name}.{extension}"] + ["/user/{user-id}/orders"]] +``` + +Having multiple terminators after a bracket path-path parameter with identical path prefix will cause a compile-time error at router creation: + +```clj +[["/files/file-{name}.pdf"] ;; terminator \. + ["/files/file-{name}-{version}.pdf"]] ;; terminator \- +``` + +### Slash Free Routing + +```clj +[["broker.{customer}.{device}.{*data}"] + ["events.{target}.{type}"]] +``` + ### Generating routes Routes are just data, so it's easy to create them programmatically: @@ -68,7 +113,7 @@ Routes are just data, so it's easy to create them programmatically: ["/api" {:interceptors [::api ::db]} (for [[type interceptor] actions :let [path (str "/" (name interceptor)) - method (condp = type + method (case type :query :get :command :post)]] [path {method {:interceptors [interceptor]}}])]) @@ -84,8 +129,3 @@ Routes are just data, so it's easy to create them programmatically: ; ["/add-user" {:post {:interceptors [add-user]}}] ; ["/add-order" {:post {:interceptors [add-order]}}])] ``` - -### Encoding - -Reitit does not apply any encoding to your paths. If you need that, you must encode them yourself. -E.g., `/foo bar` should be `/foo%20bar`. diff --git a/doc/basics/router.md b/doc/basics/router.md index 684c8ff8..3ac30136 100644 --- a/doc/basics/router.md +++ b/doc/basics/router.md @@ -41,10 +41,74 @@ The flattened route tree: ; ["/api/user/:id" {:name :user/user}]] ``` +With a router instance, we can do [Path-based routing](path_based_routing.md) or [Name-based (Reverse) routing](name_based_routing.md). + +## More details + +Router options: + +```clj +(r/options router) +{:lookup #object[...] + :expand #object[...] + :coerce #object[...] + :compile #object[...] + :conflicts #object[...]} +``` + +Route names: + +```clj +(r/route-names router) +; [:user/ping :user/user] +``` + +The compiled route tree: + +```clj +(r/routes router) +; [["/api/ping" {:name :user/ping} nil] +; ["/api/user/:id" {:name :user/user} nil]] +``` + +### Composing + +As routes are defined as plain data, it's easy to merge multiple route trees into a single router + +```clj +(def user-routes + [["/users" ::users] + ["/users/:id" ::user]]) + +(def admin-routes + ["/admin" + ["/ping" ::ping] + ["/users" ::users]]) + +(r/router + [admin-routes + user-routes]) +``` + +Merged route tree: + +```clj +(r/routes router) +; [["/admin/ping" {:name :user/ping}] +; ["/admin/db" {:name :user/db}] +; ["/users" {:name :user/users}] +; ["/users/:id" {:name :user/user}]] +``` + +More details on [composing routers](../advanced/composing_routers.md). + ### Behind the scenes + When router is created, the following steps are done: * route tree is flattened -* route arguments are expanded (via `reitit.core/Expand` protocol) and optionally coerced -* [route conflicts](advanced/route_conflicts.md) are resolved -* route tree is compiled -* actual [router implementation](../advanced/different_routers.md) is selected and created +* route arguments are expanded (via `:expand` option) +* routes are coerced (via `:coerce` options) +* route tree is compiled (via `:compile` options) +* [route conflicts](advanced/route_conflicts.md) are resolved (via `:conflicts` options) +* optionally, route data is validated (via `:validate` options) +* [router implementation](../advanced/different_routers.md) is automatically selected (or forced via `:router` options) and created From d0cfdf304a41cc45624841838d2be22ad942a17a Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 10 Feb 2019 13:17:34 +0200 Subject: [PATCH 43/51] Polish --- modules/reitit-core/java-src/reitit/Trie.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 7760a113..1c60987b 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -12,8 +12,7 @@ import java.util.*; public class Trie { - private static String decode(char[] chars, int begin, int end, boolean hasPercent, boolean hasPlus) { - final String s = new String(chars, begin, end - begin); + private static String decode(String s, boolean hasPercent, boolean hasPlus) { try { if (hasPercent) { return URLDecoder.decode(hasPlus ? s.replace("+", "%2B") : s, "UTF-8"); @@ -36,7 +35,7 @@ public class Trie { break; } } - return decode(chars, begin, end, hasPercent, hasPlus); + return decode(new String(chars, begin, end - begin), hasPercent, hasPlus); } public static class Match { @@ -114,18 +113,16 @@ public class Trie { } static final class DataMatcher implements Matcher { - private final IPersistentMap params; - private final Object data; + private final Match match; DataMatcher(IPersistentMap params, Object data) { - this.params = params; - this.data = data; + this.match = new Match(params, data); } @Override public Match match(int i, int max, char[] path) { if (i == max) { - return new Match(params, data); + return match; } return null; } @@ -142,7 +139,7 @@ public class Trie { @Override public String toString() { - return (data != null ? data.toString() : "nil"); + return (match.data != null ? match.data.toString() : "nil"); } } From aea8a8f2d536d4d58521e7690c15a4d5dc2165b8 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sun, 10 Feb 2019 16:07:16 +0200 Subject: [PATCH 44/51] Emoji routing --- test/cljc/reitit/core_test.cljc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/cljc/reitit/core_test.cljc b/test/cljc/reitit/core_test.cljc index 4c80611e..e2920342 100644 --- a/test/cljc/reitit/core_test.cljc +++ b/test/cljc/reitit/core_test.cljc @@ -102,6 +102,7 @@ ["/files/file-{name}.html" ::html] ["/files/file-{name}.json" ::json] ["/{eskon}/{saum}/pium\u2215paum" ::loru] + ["/{🌈}🤔/🎈" ::emoji] ["/extra-end}s-are/ok" ::bracket]] {:router r}) by-path #(-> router (r/match-by-path %) ((juxt (comp :name :data) :path-params)))] @@ -121,6 +122,7 @@ (is (= [::html {:name "10"}] (by-path "/files/file-10.html"))) (is (= [::loru {:eskon "viitan", :saum "aa"}] (by-path "/viitan/aa/pium\u2215paum"))) (is (= [nil nil] (by-path "/ei/osu/pium/paum"))) + (is (= [::emoji {:🌈 "brackets"}] (by-path "/brackets🤔/🎈"))) (is (= [::bracket {}] (by-path "/extra-end}s-are/ok"))))) (testing "invalid syntax fails fast" From 358f6d89328dd744a2e0d9b624a43e54515376ef Mon Sep 17 00:00:00 2001 From: Miikka Koskinen Date: Wed, 27 Feb 2019 13:21:27 +0200 Subject: [PATCH 45/51] Update doc/basics/route_syntax.md Co-Authored-By: ikitommi --- doc/basics/route_syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/basics/route_syntax.md b/doc/basics/route_syntax.md index cb3216a2..0f05f317 100644 --- a/doc/basics/route_syntax.md +++ b/doc/basics/route_syntax.md @@ -4,7 +4,7 @@ Routes are defined as vectors of String path and optional (non-sequential) route Routes can be wrapped in vectors and lists and `nil` routes are ignored. -Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since `0.4.0`, parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. The non-bracket syntax might be deprecated later. +Paths can have path-parameters (`:id`) or catch-all-parameters (`*path`). Since version `0.4.0`, parameters can also be wrapped in brackets, enabling use of qualified keywords `{user/id}`, `{*user/path}`. The non-bracket syntax might be deprecated later. ### Examples From 778a8b97b96d8006beb9e7d93b3aae0819e3d7c8 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 09:53:50 +0200 Subject: [PATCH 46/51] Review small fixes --- doc/basics/route_data.md | 4 +-- modules/reitit-core/java-src/reitit/Trie.java | 2 +- modules/reitit-core/src/reitit/coercion.cljc | 8 ++--- .../reitit-core/src/reitit/interceptor.cljc | 2 +- .../reitit-core/src/reitit/middleware.cljc | 2 +- .../src/reitit/coercion/schema.cljc | 8 ++--- .../reitit-spec/src/reitit/coercion/spec.cljc | 8 ++--- perf-test/clj/reitit/bide_perf_test.clj | 32 ++----------------- 8 files changed, 19 insertions(+), 47 deletions(-) diff --git a/doc/basics/route_data.md b/doc/basics/route_data.md index b615ab7c..2ea2f50e 100644 --- a/doc/basics/route_data.md +++ b/doc/basics/route_data.md @@ -147,7 +147,7 @@ Expanded routes: ``` -## Expansion +## Customizing Expansion By default, router `:expand` option has value `r/expand` function, backed by a `r/Expand` protocol. Expansion can be customized either by swapping the `:expand` implementation or by extending the Protocol. `r/Expand` implementations can be recursive. @@ -169,4 +169,4 @@ Page [shared routes](../advanced/shared_routes.md#using-custom-expander) has an ## Route data validation -See [Route data validation](route_data_validation.md), which enables an architecture of both [adaptive and principled](https://youtu.be/x9pxbnFC4aQ?t=1907) components. +See [Route data validation](route_data_validation.md). diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 1c60987b..adc220e5 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -261,7 +261,7 @@ public class Trie { @Override public int depth() { - return Arrays.stream(childs).mapToInt(Matcher::depth).max().orElseThrow(NoSuchElementException::new); + return Arrays.stream(childs).mapToInt(Matcher::depth).max().orElseThrow(NoSuchElementException::new) + 1; } @Override diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 01144de7..b9da1519 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -12,7 +12,7 @@ "Pluggable coercion protocol" (-get-name [this] "Keyword name for the coercion") (-get-options [this] "Coercion options") - (-get-apidocs [this spesification data] "Returns api documentation") + (-get-apidocs [this specification data] "Returns api documentation") (-compile-model [this model name] "Compiles a model") (-open-model [this model] "Returns a new model which allows extra keys in maps") (-encode-error [this error] "Converts error in to a serializable format") @@ -136,14 +136,14 @@ ;; api-docs ;; -(defn get-apidocs [this spesification data] +(defn get-apidocs [this specification data] (let [swagger-parameter {:query :query :body :body :form :formData :header :header :path :path :multipart :formData}] - (case spesification + (case specification :swagger (->> (update data :parameters @@ -152,7 +152,7 @@ (map (fn [[k v]] [(swagger-parameter k) v])) (filter first) (into {})))) - (-get-apidocs this spesification))))) + (-get-apidocs this specification))))) ;; ;; integration diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc index 67b41510..7ef698df 100644 --- a/modules/reitit-core/src/reitit/interceptor.cljc +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -44,7 +44,7 @@ "Available interceptors in registry:\n" (with-out-str (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) - "see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n") + "See [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n") {:id this :registry registry}))) diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index f4eaced8..44cf1a17 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -28,7 +28,7 @@ "Available middleware in registry:\n" (with-out-str (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) - "see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n") + "See [reitit.middleware/router] on how to add middleware to the registry.\n") "\n") {:id this :registry registry}))) diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index 9bd512f6..1985ba72 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -46,9 +46,9 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) - (-get-apidocs [this spesification {:keys [parameters responses]}] + (-get-apidocs [this specification {:keys [parameters responses]}] ;; TODO: this looks identical to spec, refactor when schema is done. - (case spesification + (case specification :swagger (swagger/swagger-spec (merge (if parameters @@ -69,8 +69,8 @@ $))]))}))) (throw (ex-info - (str "Can't produce Schema apidocs for " spesification) - {:type spesification, :coercion :schema})))) + (str "Can't produce Schema apidocs for " specification) + {:type specification, :coercion :schema})))) (-compile-model [_ model _] model) (-open-model [_ schema] (st/open-schema schema)) (-encode-error [_ error] diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 72086a61..6d106187 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -91,8 +91,8 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) - (-get-apidocs [this spesification {:keys [parameters responses]}] - (case spesification + (-get-apidocs [this specification {:keys [parameters responses]}] + (case specification :swagger (swagger/swagger-spec (merge (if parameters @@ -113,8 +113,8 @@ $))]))}))) (throw (ex-info - (str "Can't produce Spec apidocs for " spesification) - {:spesification spesification, :coercion :spec})))) + (str "Can't produce Spec apidocs for " specification) + {:specification specification, :coercion :spec})))) (-compile-model [_ model name] (into-spec model name)) (-open-model [_ spec] spec) diff --git a/perf-test/clj/reitit/bide_perf_test.clj b/perf-test/clj/reitit/bide_perf_test.clj index fa05247e..495b455b 100644 --- a/perf-test/clj/reitit/bide_perf_test.clj +++ b/perf-test/clj/reitit/bide_perf_test.clj @@ -207,39 +207,11 @@ (routing-test2) (reverse-routing-test)) -(import '[reitit Trie]) - -(set! *warn-on-reflection* true) (comment - (let [trie ] + (import '[reitit Trie]) + (set! *warn-on-reflection* true) - - (println - (Trie/lookup trie "/auth/login")) - - ;; 27ns - (cc/quick-bench - (dotimes [_ 1000] - (Trie/lookup trie "/auth/login"))) - - (println - (Trie/lookup trie "/auth/recovery/token/123")) - - ;; 82ns - (cc/quick-bench - (dotimes [_ 1000] - (Trie/lookup trie "/auth/recovery/token/123"))) - - (println - (Trie/lookup trie "/workspace/1/1")) - - ;; 96ns - (cc/quick-bench - (dotimes [_ 1000] - (Trie/lookup trie "/workspace/1/1"))))) - -(comment (let [trie (Trie/linearMatcher [(Trie/staticMatcher "/auth/" (Trie/linearMatcher From 3aae55bd8cb450faf6b029b23b5f6a9eb22c2bdf Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 09:54:44 +0200 Subject: [PATCH 47/51] Welcome TrieCompiler --- modules/reitit-core/src/reitit/core.cljc | 28 +- modules/reitit-core/src/reitit/trie.cljc | 247 +++++++++++------- .../clj/reitit/prefix_tree_perf_test.clj | 6 +- test/cljc/reitit/trie_test.cljc | 28 +- 4 files changed, 185 insertions(+), 124 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 7d89aba1..5090b60b 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -97,11 +97,16 @@ (defn linear-router "Creates a linear-router from resolved routes and optional - expanded options. See [[router]] for available options." + expanded options. See [[router]] for available options, plus the following: + + | key | description | + | -----------------------------|-------------| + | `:reitit.core/trie-compiler` | Optional trie-compiler." ([compiled-routes] (linear-router compiled-routes {})) ([compiled-routes opts] - (let [names (impl/find-names compiled-routes opts) + (let [compiler (::trie-compiler opts (trie/compiler)) + names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] (let [{:keys [path-params] :as route} (impl/parse p) @@ -113,7 +118,8 @@ [[] {}] compiled-routes) lookup (impl/fast-map nl) - matcher (trie/linear-matcher pl) + matcher (trie/linear-matcher compiler pl) + match-by-path (trie/matcher matcher compiler) routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify @@ -129,7 +135,7 @@ (route-names [_] names) (match-by-path [_ path] - (if-let [match (trie/lookup matcher path)] + (if-let [match (match-by-path path)] (-> (:data match) (assoc :path-params (:params match)) (assoc :path path)))) @@ -186,11 +192,16 @@ (defn trie-router "Creates a special prefix-tree router from resolved routes and optional - expanded options. See [[router]] for available options." + expanded options. See [[router]] for available options, plus the following: + + | key | description | + | -----------------------------|-------------| + | `:reitit.core/trie-compiler` | Optional trie-compiler." ([compiled-routes] (trie-router compiled-routes {})) ([compiled-routes opts] - (let [names (impl/find-names compiled-routes opts) + (let [compiler (::trie-compiler opts (trie/compiler)) + names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] (let [{:keys [path-params] :as route} (impl/parse p) @@ -201,7 +212,8 @@ (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) - pl (trie/compile pl) + matcher (trie/compile pl compiler) + match-by-path (trie/matcher matcher compiler) lookup (impl/fast-map nl) routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} @@ -218,7 +230,7 @@ (route-names [_] names) (match-by-path [_ path] - (if-let [match (trie/lookup pl path)] + (if-let [match (match-by-path path)] (-> (:data match) (assoc :path-params (:params match)) (assoc :path path)))) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index cd04da2e..4cc7ec71 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -19,6 +19,15 @@ (depth [this]) (length [this])) +(defprotocol TrieCompiler + (data-matcher [this params data]) + (static-matcher [this path matcher]) + (wild-matcher [this key end matcher]) + (catch-all-matcher [this key params data]) + (linear-matcher [this matchers]) + (prettify [this matcher]) + (path-matcher [this matcher])) + (defn assoc-param [match k v] (let [params (:params match)] (assoc match :params (assoc params k v)))) @@ -168,82 +177,113 @@ (update :children dissoc "")) node'))) -#?(:cljs - (defn decode [path start end percent?] - (let [param (subs path start end)] - (if percent? (js/decodeURIComponent param) param)))) - -(defn data-matcher [params data] - #?(:clj (Trie/dataMatcher params data) - :cljs (let [match (->Match params data)] - (reify Matcher - (match [_ i max _] - (if (= i max) - match)) - (view [_] data) - (depth [_] 1) - (length [_]))))) - -(defn static-matcher [path matcher] - #?(:clj (Trie/staticMatcher ^String path ^Trie$Matcher matcher) - :cljs (let [size (count path)] - (reify Matcher - (match [_ i max p] - (if-not (< max (+ i size)) - (loop [j 0] - (if (= j size) - (match matcher (+ i size) max p) - (if (= (get p (+ i j)) (get path j)) - (recur (inc j))))))) - (view [_] [path (view matcher)]) - (depth [_] (inc (depth matcher))) - (length [_] (count path)))))) - -(defn wild-matcher [key end matcher] - #?(:clj (Trie/wildMatcher key (if end (Character. end)) matcher) - :cljs (reify Matcher - (match [_ i max path] - (if (and (< i max) (not= (get path i) end)) - (loop [percent? false, j i] - (if (= max j) - (if-let [match (match matcher max max path)] - (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-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 params data] - #?(:clj (Trie/catchAllMatcher key params data) - :cljs (let [match (->Match params data)] - (reify Matcher - (match [_ i max path] - (if (< i max) (assoc-param match key (decode path i max true)))) - (view [_] [key [data]]) - (depth [_] 1) - (length [_]))))) - -(defn linear-matcher [matchers] - #?(:clj (Trie/linearMatcher matchers) - :cljs (let [matchers (vec (reverse (sort-by (juxt depth length) matchers))) - size (count matchers)] - (reify Matcher - (match [_ i max path] - (loop [j 0] - (if (< j size) - (or (match (get matchers j) i max path) - (recur (inc j)))))) - (view [_] (mapv view matchers)) - (depth [_] (apply max 0 (map depth matchers))) - (length [_]))))) +(defn decode [path start end percent?] + (let [param (subs path start end)] + (if percent? + #?(:cljs (js/decodeURIComponent param) + :clj (URLDecoder/decode + (if (.contains ^String param "+") + (.replace ^String param "+" "%2B") + param) + "UTF-8")) + param))) ;; -;; public api +;; Compilers +;; + +(defn clojure-trie-compiler [] + (reify + TrieCompiler + (data-matcher [_ params data] + (let [match (->Match params data)] + (reify Matcher + (match [_ i max _] + (if (= i max) + match)) + (view [_] data) + (depth [_] 1) + (length [_])))) + (static-matcher [_ path matcher] + (let [size (count path)] + (reify Matcher + (match [_ i max p] + (if-not (< max (+ i size)) + (loop [j 0] + (if (= j size) + (match matcher (+ i size) max p) + (if (= (get p (+ i j)) (get path j)) + (recur (inc j))))))) + (view [_] [path (view matcher)]) + (depth [_] (inc (depth matcher))) + (length [_] (count path))))) + (wild-matcher [_ key end matcher] + (reify Matcher + (match [_ i max path] + (if (and (< i max) (not= (get path i) end)) + (loop [percent? false, j i] + (if (= max j) + (if-let [match (match matcher max max path)] + (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-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 [_]))) + (catch-all-matcher [_ key params data] + (let [match (->Match params data)] + (reify Matcher + (match [_ i max path] + (if (< i max) (assoc-param match key (decode path i max true)))) + (view [_] [key [data]]) + (depth [_] 1) + (length [_])))) + (linear-matcher [_ matchers] + (let [matchers (vec (reverse (sort-by (juxt depth length) matchers))) + size (count matchers)] + (reify Matcher + (match [_ i max path] + (loop [j 0] + (if (< j size) + (or (match (get matchers j) i max path) + (recur (inc j)))))) + (view [_] (mapv view matchers)) + (depth [_] (inc (apply max 0 (map depth matchers)))) + (length [_])))) + (prettify [_ matcher] + (view matcher)) + (path-matcher [_ matcher] + (fn [path] + (if-let [match (match matcher 0 (count path) path)] + (->Match (:params match) (:data match))))))) + +#?(:clj + (defn java-trie-compiler [] + (reify + TrieCompiler + (data-matcher [_ params data] + (Trie/dataMatcher params data)) + (static-matcher [_ path matcher] + (Trie/staticMatcher ^String path ^Trie$Matcher matcher)) + (wild-matcher [_ key end matcher] + (Trie/wildMatcher key (if end (Character. end)) matcher)) + (catch-all-matcher [_ key params data] + (Trie/catchAllMatcher key params data)) + (linear-matcher [_ matchers] + (Trie/linearMatcher matchers)) + (prettify [_ matcher] + (-> matcher str read-string eval)) + (path-matcher [_ matcher] + (fn [path] + (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] + (->Match (.params match) (.data match)))))))) + +;; +;; Managing Tries ;; (defn insert @@ -259,33 +299,42 @@ params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] (-insert (or node (-node {})) (split-path path) params data)))) -(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 params data))) - (into (for [[p c] children] (static-matcher p (compile c)))) - (into - (for [[p c] wilds] - (let [p (:value p) - ends (ends c)] - (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) params (:data c)))))] - (cond - (> (count matchers) 1) (linear-matcher matchers) - (= (count matchers) 1) (first matchers) - :else (data-matcher {} nil)))) +(defn compiler [] + #?(:cljs (clojure-trie-compiler) + :clj (java-trie-compiler))) -(defn pretty [matcher] - #?(:clj (-> matcher str read-string eval) - :cljs (view matcher))) +(defn compile + ([options] + (compile options (compiler))) + ([{:keys [data params children wilds catch-all] :or {params {}}} compiler] + (let [ends (fn [{:keys [children]}] (or (keys children) ["/"])) + matchers (-> [] + (cond-> data (conj (data-matcher compiler params data))) + (into (for [[p c] children] (static-matcher compiler p (compile c compiler)))) + (into + (for [[p c] wilds] + (let [p (:value p) + ends (ends c)] + (if (next ends) + (ex/fail! (str "Trie compliation error: wild " p " has two terminators: " ends)) + (wild-matcher compiler p (ffirst ends) (compile c compiler)))))) + (into (for [[p c] catch-all] (catch-all-matcher compiler (:value p) params (:data c)))))] + (cond + (> (count matchers) 1) (linear-matcher compiler matchers) + (= (count matchers) 1) (first matchers) + :else (data-matcher compiler {} nil))))) -(defn lookup [matcher path] - #?(:clj (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] - (->Match (.params match) (.data match))) - :cljs (if-let [match (match matcher 0 (count path) path)] - (->Match (:params match) (:data match))))) +(defn pretty + ([trie] + (pretty trie (compiler))) + ([trie compiler] + (prettify compiler trie))) + +(defn matcher + ([trie] + (matcher trie (compiler))) + ([trie compiler] + (path-matcher compiler trie))) ;; ;; spike diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index aeed5516..63da20d7 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -98,7 +98,7 @@ ;; 0.8µs (return route-data) ;; 0.8µs (fix payloads) #_(cc/quick-bench - (trie/lookup reitit-tree "/v1/orgs/1/topics" {})) + (trie/matcher reitit-tree "/v1/orgs/1/topics" {})) ;; 0.9µs (initial) ;; 0.5µs (protocols) @@ -114,7 +114,7 @@ ;; 0.30µs (iterate arrays) ;; 0.28µs (list-params) (cc/quick-bench - (trie/lookup trie-matcher "/v1/orgs/1/topics"))) + (trie/matcher trie-matcher "/v1/orgs/1/topics"))) (comment (bench!)) @@ -123,6 +123,6 @@ (comment (p/lookup pedestal-tree "/v1/orgs/1/topics") - (trie/lookup trie-matcher "/v1/orgs/1/topics") + (trie/matcher trie-matcher "/v1/orgs/1/topics") #_(segment/lookup segment-matcher "/v1/orgs/1/topics")) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index 291937c5..a2575599 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -15,23 +15,23 @@ (deftest tests (is (= (trie/->Match {} {:a 1}) - (-> (trie/insert nil "/foo" {:a 1}) - (trie/compile) - (trie/lookup "/foo")))) + ((-> (trie/insert nil "/foo" {:a 1}) + (trie/compile) + (trie/matcher)) "/foo"))) (is (= (trie/->Match {} {:a 1}) - (-> (trie/insert nil "/foo" {:a 1}) - (trie/insert "/foo/*bar" {:b 1}) - (trie/compile) - (trie/lookup "/foo")))) + ((-> (trie/insert nil "/foo" {:a 1}) + (trie/insert "/foo/*bar" {:b 1}) + (trie/compile) + (trie/matcher)) "/foo"))) (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")))) + ((-> (trie/insert nil "/foo" {:a 1}) + (trie/insert "/foo/*bar" {:b 1}) + (trie/compile) + (trie/matcher)) "/foo/bar"))) (is (= (trie/->Match {} {:a 1}) - (-> (trie/insert nil "" {:a 1}) - (trie/compile) - (trie/lookup ""))))) + ((-> (trie/insert nil "" {:a 1}) + (trie/compile) + (trie/matcher)) "")))) From a9c33de7d172ce0228fb4f1ed7f86266c4d7e04a Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 10:19:21 +0200 Subject: [PATCH 48/51] fix perf tests --- perf-test/clj/reitit/prefix_tree_perf_test.clj | 17 +++++++++-------- 1 file changed, 9 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 63da20d7..628f2ed8 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -70,11 +70,12 @@ nil routes)) (def trie-matcher - (trie/compile - (reduce - (fn [acc [p d]] - (trie/insert acc p d)) - nil routes))) + (trie/matcher + (trie/compile + (reduce + (fn [acc [p d]] + (trie/insert acc p d)) + nil routes)))) (defn bench! [] @@ -108,13 +109,13 @@ ;; 0.51µs (Cleanup) ;; 0.30µs (Java) #_(cc/quick-bench - (segment/lookup segment-matcher "/v1/orgs/1/topics")) + (segment/lookup segment-matcher "/v1/orgs/1/topics")) ;; 0.32µs (initial) ;; 0.30µs (iterate arrays) ;; 0.28µs (list-params) (cc/quick-bench - (trie/matcher trie-matcher "/v1/orgs/1/topics"))) + (trie-matcher "/v1/orgs/1/topics"))) (comment (bench!)) @@ -123,6 +124,6 @@ (comment (p/lookup pedestal-tree "/v1/orgs/1/topics") - (trie/matcher trie-matcher "/v1/orgs/1/topics") + (trie-matcher "/v1/orgs/1/topics") #_(segment/lookup segment-matcher "/v1/orgs/1/topics")) From df8cfed1255c450daaa14dd080291551ab1cc08f Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 12:08:04 +0200 Subject: [PATCH 49/51] Don't print average lower --- perf-test/clj/reitit/perf_utils.clj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/perf-test/clj/reitit/perf_utils.clj b/perf-test/clj/reitit/perf_utils.clj index 49d0c485..be7b8839 100644 --- a/perf-test/clj/reitit/perf_utils.clj +++ b/perf-test/clj/reitit/perf_utils.clj @@ -64,6 +64,4 @@ (do (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))) "/" - (int (/ (reduce + (map first times)) (count times))))))) + (title (str "average, mean: " (int (/ (reduce + (map first times)) (count times))))))) From f0a6ceb837b3c6b27baa74e1fcd0ddb03a305439 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 12:09:25 +0200 Subject: [PATCH 50/51] rename internals for clarity --- modules/reitit-core/src/reitit/core.cljc | 4 +- modules/reitit-core/src/reitit/trie.cljc | 44 +++++++++++-------- .../clj/reitit/prefix_tree_perf_test.clj | 22 ++++++---- test/cljc/reitit/trie_test.cljc | 8 ++-- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 5090b60b..ca9720d6 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -119,7 +119,7 @@ compiled-routes) lookup (impl/fast-map nl) matcher (trie/linear-matcher compiler pl) - match-by-path (trie/matcher matcher compiler) + match-by-path (trie/path-matcher matcher compiler) routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} (reify @@ -213,7 +213,7 @@ [nil {}] compiled-routes) matcher (trie/compile pl compiler) - match-by-path (trie/matcher matcher compiler) + match-by-path (trie/path-matcher matcher compiler) lookup (impl/fast-map nl) routes (impl/uncompile-routes compiled-routes)] ^{:type ::router} diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 4cc7ec71..242ff299 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -25,15 +25,15 @@ (wild-matcher [this key end matcher]) (catch-all-matcher [this key params data]) (linear-matcher [this matchers]) - (prettify [this matcher]) - (path-matcher [this matcher])) + (-pretty [this matcher]) + (-path-matcher [this matcher])) -(defn assoc-param [match 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] +(defn- common-prefix [s1 s2] (let [max (min (count s1) (count s2))] (loop [i 0] (cond @@ -177,7 +177,7 @@ (update :children dissoc "")) node'))) -(defn decode [path start end percent?] +(defn- decode [path start end percent?] (let [param (subs path start end)] (if percent? #?(:cljs (js/decodeURIComponent param) @@ -254,9 +254,9 @@ (view [_] (mapv view matchers)) (depth [_] (inc (apply max 0 (map depth matchers)))) (length [_])))) - (prettify [_ matcher] + (-pretty [_ matcher] (view matcher)) - (path-matcher [_ matcher] + (-path-matcher [_ matcher] (fn [path] (if-let [match (match matcher 0 (count path) path)] (->Match (:params match) (:data match))))))) @@ -275,9 +275,9 @@ (Trie/catchAllMatcher key params data)) (linear-matcher [_ matchers] (Trie/linearMatcher matchers)) - (prettify [_ matcher] + (-pretty [_ matcher] (-> matcher str read-string eval)) - (path-matcher [_ matcher] + (-path-matcher [_ matcher] (fn [path] (if-let [match ^Trie$Match (Trie/lookup ^Trie$Matcher matcher ^String path)] (->Match (.params match) (.data match)))))))) @@ -287,6 +287,7 @@ ;; (defn insert + "Returns a trie with routes added to it." ([routes] (insert nil routes)) ([node routes] @@ -299,11 +300,14 @@ params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] (-insert (or node (-node {})) (split-path path) params data)))) -(defn compiler [] +(defn compiler + "Returns a default [[TrieCompiler]]." + [] #?(:cljs (clojure-trie-compiler) :clj (java-trie-compiler))) (defn compile + "Returns a compiled trie, to be used with [[pretty]] or [[path-matcher]]." ([options] (compile options (compiler))) ([{:keys [data params children wilds catch-all] :or {params {}}} compiler] @@ -325,16 +329,18 @@ :else (data-matcher compiler {} nil))))) (defn pretty - ([trie] - (pretty trie (compiler))) - ([trie compiler] - (prettify compiler trie))) + "Returns a simplified EDN structure of a compiled trie for printing purposes." + ([compiled-trie] + (pretty compiled-trie (compiler))) + ([compiled-trie compiler] + (-pretty compiler compiled-trie))) -(defn matcher - ([trie] - (matcher trie (compiler))) - ([trie compiler] - (path-matcher compiler trie))) +(defn path-matcher + "Returns a function of `path -> Match` from a compiled trie." + ([compiled-trie] + (path-matcher compiled-trie (compiler))) + ([compiled-trie compiler] + (-path-matcher compiler compiled-trie))) ;; ;; spike diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index 628f2ed8..b35a102a 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -70,7 +70,7 @@ nil routes)) (def trie-matcher - (trie/matcher + (trie/path-matcher (trie/compile (reduce (fn [acc [p d]] @@ -80,8 +80,10 @@ (defn bench! [] ;; 2.3µs - #_(cc/quick-bench - (p/lookup pedestal-tree "/v1/orgs/1/topics")) + ;; 2.1µs (28.2.2019) + (cc/with-progress-reporting + (cc/bench + (p/lookup pedestal-tree "/v1/orgs/1/topics"))) ;; 3.1µs ;; 2.5µs (string equals) @@ -99,7 +101,7 @@ ;; 0.8µs (return route-data) ;; 0.8µs (fix payloads) #_(cc/quick-bench - (trie/matcher reitit-tree "/v1/orgs/1/topics" {})) + (trie/path-matcher reitit-tree "/v1/orgs/1/topics" {})) ;; 0.9µs (initial) ;; 0.5µs (protocols) @@ -111,11 +113,13 @@ #_(cc/quick-bench (segment/lookup segment-matcher "/v1/orgs/1/topics")) - ;; 0.32µs (initial) - ;; 0.30µs (iterate arrays) - ;; 0.28µs (list-params) - (cc/quick-bench - (trie-matcher "/v1/orgs/1/topics"))) + ;; 0.320µs (initial) + ;; 0.300µs (iterate arrays) + ;; 0.280µs (list-params) + ;; 0.096µs (trie) + (cc/with-progress-reporting + (cc/bench + (trie-matcher "/v1/orgs/1/topics")))) (comment (bench!)) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index a2575599..90286d77 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -17,21 +17,21 @@ (is (= (trie/->Match {} {:a 1}) ((-> (trie/insert nil "/foo" {:a 1}) (trie/compile) - (trie/matcher)) "/foo"))) + (trie/path-matcher)) "/foo"))) (is (= (trie/->Match {} {:a 1}) ((-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) - (trie/matcher)) "/foo"))) + (trie/path-matcher)) "/foo"))) (is (= (trie/->Match {:bar "bar"} {:b 1}) ((-> (trie/insert nil "/foo" {:a 1}) (trie/insert "/foo/*bar" {:b 1}) (trie/compile) - (trie/matcher)) "/foo/bar"))) + (trie/path-matcher)) "/foo/bar"))) (is (= (trie/->Match {} {:a 1}) ((-> (trie/insert nil "" {:a 1}) (trie/compile) - (trie/matcher)) "")))) + (trie/path-matcher)) "")))) From 251f547b883f9314e61a16db6986153fcf3dcb18 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 28 Feb 2019 12:16:51 +0200 Subject: [PATCH 51/51] rebase-error-fix --- modules/reitit-core/src/reitit/interceptor.cljc | 11 ++++++----- modules/reitit-core/src/reitit/middleware.cljc | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/reitit-core/src/reitit/interceptor.cljc b/modules/reitit-core/src/reitit/interceptor.cljc index 7ef698df..5b3de7d7 100644 --- a/modules/reitit-core/src/reitit/interceptor.cljc +++ b/modules/reitit-core/src/reitit/interceptor.cljc @@ -34,9 +34,10 @@ #?(:clj clojure.lang.Keyword :cljs cljs.core.Keyword) (into-interceptor [this data {:keys [::registry] :as opts}] - (or (if-let [interceptor (if registry (registry this))] - (into-interceptor interceptor data opts)) - (exception/fail! + (if-let [interceptor (if registry (registry this))] + (into-interceptor interceptor data opts) + (throw + (ex-info (str "Interceptor " this " not found in registry.\n\n" (if (seq registry) @@ -44,9 +45,9 @@ "Available interceptors in registry:\n" (with-out-str (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) - "See [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n") + "see [reitit.interceptor/router] on how to add interceptor to the registry.\n") "\n") {:id this - :registry registry}))) + :registry registry})))) #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector) diff --git a/modules/reitit-core/src/reitit/middleware.cljc b/modules/reitit-core/src/reitit/middleware.cljc index 44cf1a17..51d206be 100644 --- a/modules/reitit-core/src/reitit/middleware.cljc +++ b/modules/reitit-core/src/reitit/middleware.cljc @@ -18,9 +18,10 @@ #?(:clj clojure.lang.Keyword :cljs cljs.core.Keyword) (into-middleware [this data {:keys [::registry] :as opts}] - (or (if-let [middleware (if registry (registry this))] - (into-middleware middleware data opts)) - (exception/fail! + (if-let [middleware (if registry (registry this))] + (into-middleware middleware data opts) + (throw + (ex-info (str "Middleware " this " not found in registry.\n\n" (if (seq registry) @@ -28,9 +29,9 @@ "Available middleware in registry:\n" (with-out-str (pprint/print-table [:id :description] (for [[k v] registry] {:id k :description v})))) - "See [reitit.middleware/router] on how to add middleware to the registry.\n") "\n") + "see [reitit.middleware/router] on how to add middleware to the registry.\n") "\n") {:id this - :registry registry}))) + :registry registry})))) #?(:clj clojure.lang.APersistentVector :cljs cljs.core.PersistentVector)