From 5ab1f2829e17995dc98a3239b0d31c3d2437bd49 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 7 Mar 2019 08:17:17 +0200 Subject: [PATCH 1/4] Decode in single sweep, 8% faster --- CHANGELOG.md | 4 ++-- modules/reitit-core/java-src/reitit/Trie.java | 6 +++++- perf-test/clj/reitit/go_perf_test.clj | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acfa86c7..22c439f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * `"/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 + * [up to 2x faster](https://metosin.github.io/reitit/performance.html) on the JVM * **BREAKING**: `reitit.spec/validate-spec!` has been renamed to `validate` * With `clojure.spec` coercion, values flow through both `st/coerce` & `st/conform` yielding better error messages. Original issue in [compojure-api](https://github.com/metosin/compojure-api/issues/409). @@ -59,7 +59,7 @@ ### `reitit-frontend` -* **BREAKING**: Frontend controllers redesigned +* Frontend controllers redesigned * Controller `:params` function has been deprecated * Controller `:identity` function works the same as `:params` * New `:parameters` option can be used to declare which parameters diff --git a/modules/reitit-core/java-src/reitit/Trie.java b/modules/reitit-core/java-src/reitit/Trie.java index 65f98a42..953a70ad 100644 --- a/modules/reitit-core/java-src/reitit/Trie.java +++ b/modules/reitit-core/java-src/reitit/Trie.java @@ -160,10 +160,14 @@ public class Trie { @Override public Match match(int i, int max, char[] path) { + boolean hasPercent = false; + boolean hasPlus = false; if (i < max && path[i] != end) { int stop = max; for (int j = i; j < max; j++) { final char c = path[j]; + hasPercent = hasPercent || c == '%'; + hasPlus = hasPlus || c == '+'; if (c == end) { stop = j; break; @@ -171,7 +175,7 @@ public class Trie { } final Match m = child.match(stop, max, path); if (m != null) { - m.params = m.params.assoc(key, decode(path, i, stop)); + m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus)); } return m; } diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 59838449..176cde7c 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -334,6 +334,7 @@ ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 273ns (trie-router, no injects, direct-data) ;; 256ns (trie-router, pre-defined parameters) + ;; 237ns (trie-router, single-sweep wild-params) (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})] (title "param") (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) @@ -365,6 +366,6 @@ (do (require '[clj-async-profiler.core :as prof]) (prof/profile - (dotimes [_ 1000000] + (dotimes [_ 10000000] (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}))) (prof/serve-files 8080))) From 55a5133e85c65a8e9e622c4abc764d1062acd492 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 7 Mar 2019 08:19:28 +0200 Subject: [PATCH 2/4] Rename `:reitit.core/trie-compiler` -> `:reitit.trie/trie-compiler` --- modules/reitit-core/src/reitit/core.cljc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 4b121eda..f939b12b 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -79,11 +79,11 @@ | key | description | | -----------------------------|-------------| - | `:reitit.core/trie-compiler` | Optional trie-compiler." + | `:reitit.trie/trie-compiler` | Optional trie-compiler. ([compiled-routes] (linear-router compiled-routes {})) ([compiled-routes opts] - (let [compiler (::trie-compiler opts (trie/compiler)) + (let [compiler (::trie/trie-compiler opts (trie/compiler)) names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] @@ -174,11 +174,11 @@ | key | description | | -----------------------------|-------------| - | `:reitit.core/trie-compiler` | Optional trie-compiler." + | `:reitit.trie/trie-compiler` | Optional trie-compiler. ([compiled-routes] (trie-router compiled-routes {})) ([compiled-routes opts] - (let [compiler (::trie-compiler opts (trie/compiler)) + (let [compiler (::trie/trie-compiler opts (trie/compiler)) names (impl/find-names compiled-routes opts) [pl nl] (reduce (fn [[pl nl] [p {:keys [name] :as data} result]] From e41a50cb80f00078c57516fc301b80e793d5e928 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 7 Mar 2019 08:20:41 +0200 Subject: [PATCH 3/4] 20% faster wildcard parameters with records --- modules/reitit-core/src/reitit/core.cljc | 6 ++++-- modules/reitit-core/src/reitit/trie.cljc | 16 +++++++++++++++- perf-test/clj/reitit/go_perf_test.clj | 5 ++++- perf-test/clj/reitit/prefix_tree_perf_test.clj | 3 ++- test/cljc/reitit/trie_test.cljc | 9 ++++++++- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index f939b12b..1025618a 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -80,6 +80,7 @@ | key | description | | -----------------------------|-------------| | `:reitit.trie/trie-compiler` | Optional trie-compiler. + | `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)." ([compiled-routes] (linear-router compiled-routes {})) ([compiled-routes opts] @@ -91,7 +92,7 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result (impl/url-decode-coll %) path-params))] - [(conj pl (-> (trie/insert nil p (->Match p data result nil nil)) (trie/compile))) + [(conj pl (-> (trie/insert nil p (->Match p data result nil nil) opts) (trie/compile))) (if name (assoc nl name f) nl)])) [[] {}] compiled-routes) @@ -175,6 +176,7 @@ | key | description | | -----------------------------|-------------| | `:reitit.trie/trie-compiler` | Optional trie-compiler. + | `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)." ([compiled-routes] (trie-router compiled-routes {})) ([compiled-routes opts] @@ -186,7 +188,7 @@ f #(if-let [path (impl/path-for route %)] (->Match p data result (impl/url-decode-coll %) path) (->PartialMatch p data result (impl/url-decode-coll %) path-params))] - [(trie/insert pl p (->Match p data result nil nil)) + [(trie/insert pl p (->Match p data result nil nil) opts) (if name (assoc nl name f) nl)])) [nil {}] compiled-routes) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index a6ba6251..501693b7 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -288,6 +288,18 @@ ;; Managing Tries ;; +#?(:clj + (def record-parameters + "Memoized function to transform parameters into runtime generated Record." + (memoize + (fn [params] + (let [fields (keys params)] + (if (some qualified-keyword? fields) + params + (let [name (gensym "PathParams") + ctor (symbol (str "map->" name))] + (eval `(do (defrecord ~name ~(mapv symbol fields)) (~ctor {})))))))))) + (defn insert "Returns a trie with routes added to it." ([routes] @@ -298,8 +310,10 @@ (insert acc p d)) node routes)) ([node path data] + (insert node path data nil)) + ([node path data {::keys [parameters] :or {parameters identity}}] (let [parts (split-path path) - params (zipmap (->> parts (remove string?) (map :value)) (repeat nil))] + params (parameters (zipmap (->> parts (remove string?) (map :value)) (repeat nil)))] (-insert (or node (-node {})) (split-path path) path params data)))) (defn compiler diff --git a/perf-test/clj/reitit/go_perf_test.clj b/perf-test/clj/reitit/go_perf_test.clj index 176cde7c..4e432c2e 100644 --- a/perf-test/clj/reitit/go_perf_test.clj +++ b/perf-test/clj/reitit/go_perf_test.clj @@ -2,6 +2,7 @@ (:require [criterium.core :as cc] [reitit.perf-utils :refer :all] [reitit.ring :as ring] + [reitit.trie :as trie] [clojure.string :as str])) ;; @@ -295,7 +296,8 @@ (def app (ring/ring-handler (ring/router - (reduce (partial add h) [] routes)) + (reduce (partial add h) [] routes) + {::trie/parameters trie/record-parameters}) (ring/create-default-handler) {:inject-match? false, :inject-router? false})) @@ -335,6 +337,7 @@ ;; 273ns (trie-router, no injects, direct-data) ;; 256ns (trie-router, pre-defined parameters) ;; 237ns (trie-router, single-sweep wild-params) + ;; 191ns (trie-router, record 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))) diff --git a/perf-test/clj/reitit/prefix_tree_perf_test.clj b/perf-test/clj/reitit/prefix_tree_perf_test.clj index b35a102a..0e6e2091 100644 --- a/perf-test/clj/reitit/prefix_tree_perf_test.clj +++ b/perf-test/clj/reitit/prefix_tree_perf_test.clj @@ -74,7 +74,7 @@ (trie/compile (reduce (fn [acc [p d]] - (trie/insert acc p d)) + (trie/insert acc p d {::trie/parameters trie/record-parameters})) nil routes)))) (defn bench! [] @@ -117,6 +117,7 @@ ;; 0.300µs (iterate arrays) ;; 0.280µs (list-params) ;; 0.096µs (trie) + ;; 0.083µs (trie, record-params, faster decode) (cc/with-progress-reporting (cc/bench (trie-matcher "/v1/orgs/1/topics")))) diff --git a/test/cljc/reitit/trie_test.cljc b/test/cljc/reitit/trie_test.cljc index 90286d77..1592123c 100644 --- a/test/cljc/reitit/trie_test.cljc +++ b/test/cljc/reitit/trie_test.cljc @@ -34,4 +34,11 @@ (is (= (trie/->Match {} {:a 1}) ((-> (trie/insert nil "" {:a 1}) (trie/compile) - (trie/path-matcher)) "")))) + (trie/path-matcher)) ""))) + + #?(:clj + (let [match ((-> (trie/insert nil "/:a" {:a 1} {::trie/parameters trie/record-parameters}) + (trie/compile) + (trie/path-matcher)) "/a")] + (is (record? (:params match))) + (is (= (trie/->Match {:a "a"} {:a 1}) (update match :params (partial into {}))))))) From 1d5d5f663b61e95112d4620bb825f8ad1a02f58d Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 7 Mar 2019 08:49:26 +0200 Subject: [PATCH 4/4] Support Clojure 1.9.0 --- modules/reitit-core/src/reitit/trie.cljc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/reitit-core/src/reitit/trie.cljc b/modules/reitit-core/src/reitit/trie.cljc index 501693b7..8af5877d 100644 --- a/modules/reitit-core/src/reitit/trie.cljc +++ b/modules/reitit-core/src/reitit/trie.cljc @@ -296,9 +296,9 @@ (let [fields (keys params)] (if (some qualified-keyword? fields) params - (let [name (gensym "PathParams") - ctor (symbol (str "map->" name))] - (eval `(do (defrecord ~name ~(mapv symbol fields)) (~ctor {})))))))))) + (let [sym (gensym "PathParams") + ctor (symbol (str "map->" sym))] + (eval `(do (defrecord ~sym ~(mapv (comp symbol name) fields)) (~ctor {})))))))))) (defn insert "Returns a trie with routes added to it."