Merge pull request #232 from metosin/SingleSweepJavaTrieDecode

Trie polish, 25% faster wildcard routes.
This commit is contained in:
Tommi Reiman 2019-03-07 09:01:05 +02:00 committed by GitHub
commit 38cbb5d2f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 46 additions and 14 deletions

View file

@ -12,7 +12,7 @@
* `"/file-:id/topics"` (free start, ends at slash) * `"/file-:id/topics"` (free start, ends at slash)
* `"/file-{name}.html"` (free start & end) * `"/file-{name}.html"` (free start & end)
* backed by a new `:trie-router`, replacing `:segment-router` * 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` * **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). * 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` ### `reitit-frontend`
* **BREAKING**: Frontend controllers redesigned * Frontend controllers redesigned
* Controller `:params` function has been deprecated * Controller `:params` function has been deprecated
* Controller `:identity` function works the same as `:params` * Controller `:identity` function works the same as `:params`
* New `:parameters` option can be used to declare which parameters * New `:parameters` option can be used to declare which parameters

View file

@ -160,10 +160,14 @@ public class Trie {
@Override @Override
public Match match(int i, int max, char[] path) { public Match match(int i, int max, char[] path) {
boolean hasPercent = false;
boolean hasPlus = false;
if (i < max && path[i] != end) { if (i < max && path[i] != end) {
int stop = max; int stop = max;
for (int j = i; j < max; j++) { for (int j = i; j < max; j++) {
final char c = path[j]; final char c = path[j];
hasPercent = hasPercent || c == '%';
hasPlus = hasPlus || c == '+';
if (c == end) { if (c == end) {
stop = j; stop = j;
break; break;
@ -171,7 +175,7 @@ public class Trie {
} }
final Match m = child.match(stop, max, path); final Match m = child.match(stop, max, path);
if (m != null) { if (m != null) {
m.params = m.params.assoc(key, decode(path, i, stop)); m.params = m.params.assoc(key, decode(new String(path, i, stop - i), hasPercent, hasPlus));
} }
return m; return m;
} }

View file

@ -79,11 +79,12 @@
| key | description | | key | description |
| -----------------------------|-------------| | -----------------------------|-------------|
| `:reitit.core/trie-compiler` | Optional trie-compiler." | `:reitit.trie/trie-compiler` | Optional trie-compiler.
| `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)."
([compiled-routes] ([compiled-routes]
(linear-router compiled-routes {})) (linear-router compiled-routes {}))
([compiled-routes opts] ([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) names (impl/find-names compiled-routes opts)
[pl nl] (reduce [pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]] (fn [[pl nl] [p {:keys [name] :as data} result]]
@ -91,7 +92,7 @@
f #(if-let [path (impl/path-for route %)] f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path) (->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) 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))) [(conj pl (-> (trie/insert nil p (->Match p data result nil nil) opts) (trie/compile)))
(if name (assoc nl name f) nl)])) (if name (assoc nl name f) nl)]))
[[] {}] [[] {}]
compiled-routes) compiled-routes)
@ -174,11 +175,12 @@
| key | description | | key | description |
| -----------------------------|-------------| | -----------------------------|-------------|
| `:reitit.core/trie-compiler` | Optional trie-compiler." | `:reitit.trie/trie-compiler` | Optional trie-compiler.
| `:reitit.trie/parameters` | Optional function to transform path-parameters at creation time (default `identity`)."
([compiled-routes] ([compiled-routes]
(trie-router compiled-routes {})) (trie-router compiled-routes {}))
([compiled-routes opts] ([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) names (impl/find-names compiled-routes opts)
[pl nl] (reduce [pl nl] (reduce
(fn [[pl nl] [p {:keys [name] :as data} result]] (fn [[pl nl] [p {:keys [name] :as data} result]]
@ -186,7 +188,7 @@
f #(if-let [path (impl/path-for route %)] f #(if-let [path (impl/path-for route %)]
(->Match p data result (impl/url-decode-coll %) path) (->Match p data result (impl/url-decode-coll %) path)
(->PartialMatch p data result (impl/url-decode-coll %) path-params))] (->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)])) (if name (assoc nl name f) nl)]))
[nil {}] [nil {}]
compiled-routes) compiled-routes)

View file

@ -288,6 +288,18 @@
;; Managing Tries ;; 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 [sym (gensym "PathParams")
ctor (symbol (str "map->" sym))]
(eval `(do (defrecord ~sym ~(mapv (comp symbol name) fields)) (~ctor {}))))))))))
(defn insert (defn insert
"Returns a trie with routes added to it." "Returns a trie with routes added to it."
([routes] ([routes]
@ -298,8 +310,10 @@
(insert acc p d)) (insert acc p d))
node routes)) node routes))
([node path data] ([node path data]
(insert node path data nil))
([node path data {::keys [parameters] :or {parameters identity}}]
(let [parts (split-path path) (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)))) (-insert (or node (-node {})) (split-path path) path params data))))
(defn compiler (defn compiler

View file

@ -2,6 +2,7 @@
(:require [criterium.core :as cc] (:require [criterium.core :as cc]
[reitit.perf-utils :refer :all] [reitit.perf-utils :refer :all]
[reitit.ring :as ring] [reitit.ring :as ring]
[reitit.trie :as trie]
[clojure.string :as str])) [clojure.string :as str]))
;; ;;
@ -295,7 +296,8 @@
(def app (def app
(ring/ring-handler (ring/ring-handler
(ring/router (ring/router
(reduce (partial add h) [] routes)) (reduce (partial add h) [] routes)
{::trie/parameters trie/record-parameters})
(ring/create-default-handler) (ring/create-default-handler)
{:inject-match? false, :inject-router? false})) {:inject-match? false, :inject-router? false}))
@ -334,6 +336,8 @@
;; 277ns (trie-router, no injects, switch-case) - 690ns clojure ;; 277ns (trie-router, no injects, switch-case) - 690ns clojure
;; 273ns (trie-router, no injects, direct-data) ;; 273ns (trie-router, no injects, direct-data)
;; 256ns (trie-router, pre-defined parameters) ;; 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"})] (let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})]
(title "param") (title "param")
(assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req))) (assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req)))
@ -365,6 +369,6 @@
(do (do
(require '[clj-async-profiler.core :as prof]) (require '[clj-async-profiler.core :as prof])
(prof/profile (prof/profile
(dotimes [_ 1000000] (dotimes [_ 10000000]
(app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"}))) (app {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})))
(prof/serve-files 8080))) (prof/serve-files 8080)))

View file

@ -74,7 +74,7 @@
(trie/compile (trie/compile
(reduce (reduce
(fn [acc [p d]] (fn [acc [p d]]
(trie/insert acc p d)) (trie/insert acc p d {::trie/parameters trie/record-parameters}))
nil routes)))) nil routes))))
(defn bench! [] (defn bench! []
@ -117,6 +117,7 @@
;; 0.300µs (iterate arrays) ;; 0.300µs (iterate arrays)
;; 0.280µs (list-params) ;; 0.280µs (list-params)
;; 0.096µs (trie) ;; 0.096µs (trie)
;; 0.083µs (trie, record-params, faster decode)
(cc/with-progress-reporting (cc/with-progress-reporting
(cc/bench (cc/bench
(trie-matcher "/v1/orgs/1/topics")))) (trie-matcher "/v1/orgs/1/topics"))))

View file

@ -34,4 +34,11 @@
(is (= (trie/->Match {} {:a 1}) (is (= (trie/->Match {} {:a 1})
((-> (trie/insert nil "" {:a 1}) ((-> (trie/insert nil "" {:a 1})
(trie/compile) (trie/compile)
(trie/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 {})))))))