From 95ebdfa6a49635d794cc9b379e9633d8b899b5c0 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Thu, 17 Jan 2019 08:13:25 +0200 Subject: [PATCH] linear-router is backed by a segment-router --- CHANGELOG.md | 3 +- .../java-src/reitit/SegmentTrie.java | 4 ++ modules/reitit-core/src/reitit/core.cljc | 14 +++--- modules/reitit-core/src/reitit/impl.cljc | 50 +++++-------------- modules/reitit-core/src/reitit/segment.cljc | 5 ++ .../clj/reitit/opensensors_perf_test.clj | 14 ++++-- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7378a277..43452013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### `reitit-core` * `reitit.core/Expand` can be extended, fixes [#201](https://github.com/metosin/reitit/issues/201). -* new snappy Java-backed `SegmentTrie` routing algorithm and data structure, making wildcard routing ~2x faster on the JVM +* new snappy Java-backed `SegmentTrie` routing algorithm and data structure (for `reitit.core/segment-router`, making wildcard routing ~2x faster on the JVM +* `reitit.core/linear-router` uses the segment router behind the scenes, 2-4x faster on the JVM too. ### `reitit-ring` diff --git a/modules/reitit-core/java-src/reitit/SegmentTrie.java b/modules/reitit-core/java-src/reitit/SegmentTrie.java index 2337d893..062034e3 100644 --- a/modules/reitit-core/java-src/reitit/SegmentTrie.java +++ b/modules/reitit-core/java-src/reitit/SegmentTrie.java @@ -297,6 +297,10 @@ public class SegmentTrie { } } + 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()); } diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 24852b8f..a2f090d0 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -128,7 +128,7 @@ (mapv (comp vec (partial take 2)) routes)) (defn route-info [route] - (select-keys (impl/create route) [:path :path-parts :path-params :result :data])) + (impl/create route)) (defprotocol Router (router-name [this]) @@ -177,11 +177,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 route) + [(conj pl (-> (segment/insert nil p (->Match p data result nil nil)) (segment/compile))) (if name (assoc nl name f) nl)])) [[] {}] compiled-routes) lookup (impl/fast-map nl) + scanner (segment/scanner pl) routes (uncompile-routes compiled-routes)] ^{:type ::router} (reify @@ -197,11 +198,10 @@ (route-names [_] names) (match-by-path [_ path] - (reduce - (fn [_ ^Route route] - (if-let [path-params ((:matcher route) path)] - (reduced (->Match (:path route) (:data route) (:result route) (impl/url-decode-coll path-params) path)))) - nil pl)) + (if-let [match (segment/lookup scanner path)] + (-> (:data match) + (assoc :path-params (:path-params match)) + (assoc :path path)))) (match-by-name [_ name] (if-let [match (impl/fast-get lookup name)] (match nil))) diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc index f8deed9d..44424b88 100644 --- a/modules/reitit-core/src/reitit/impl.cljc +++ b/modules/reitit-core/src/reitit/impl.cljc @@ -66,18 +66,16 @@ (let [key (keyword token)] (-> out (update-in [:path-parts] conj key) - (update-in [:path-params] conj key) - (assoc-in [:path-constraints 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) - (assoc-in [:path-constraints key] "(.*)")))) + (update-in [:path-params] conj key)))) (update-in out [:path-parts] conj string))) (defn- parse-path - ([pattern] (parse-path {:path-parts [] :path-params [] :path-constraints {}} pattern)) + ([pattern] (parse-path {:path-parts [] :path-params #{}} pattern)) ([accumulated-info pattern] (if-let [m (re-matches #"/(.*)" pattern)] (let [[_ path] m] @@ -86,45 +84,21 @@ (str/split path #"/"))) (throw (ex-info "Routes must start from the root, so they must begin with a '/'" {:pattern pattern}))))) -;; TODO: is this correct? -(defn- re-quote [x] - #?(:clj (Pattern/quote x) - :cljs (str/replace x #"([.?*+^$[\\]\\\\(){}|-])" "\\$1"))) - -(defn- path-regex [{:keys [path-parts path-constraints] :as route}] - (let [[pp & pps] path-parts - path-parts (if (and (seq pps) (string? pp) (empty? pp)) pps path-parts)] - (re-pattern - (apply str - (interleave (repeat "/") - (map #(or (get path-constraints %) (re-quote %)) - path-parts)))))) - -(defn- path-matcher [route] - (let [{:keys [path-re path-params]} route] - (fn [path] - (when-let [m (re-matches path-re path)] - (zipmap path-params (rest m)))))) - ;; ;; Routing (c) Metosin ;; -(defrecord Route [path matcher path-parts path-params data result]) +(defrecord Route [path path-parts path-params data result]) (defn create [[path data result]] - (let [path #?(:clj (.intern ^String path) :cljs path)] - (as-> (parse-path path) $ - (assoc $ :path-re (path-regex $)) - (merge $ {:path path - :matcher (if (contains-wilds? path) - (path-matcher $) - #(if (#?(:clj .equals, :cljs =) path %) {})) - :result result - :data data}) - (dissoc $ :path-re :path-constraints) - (update $ :path-params set) - (map->Route $)))) + (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)) diff --git a/modules/reitit-core/src/reitit/segment.cljc b/modules/reitit-core/src/reitit/segment.cljc index d51022df..0ab1aca2 100644 --- a/modules/reitit-core/src/reitit/segment.cljc +++ b/modules/reitit-core/src/reitit/segment.cljc @@ -59,6 +59,11 @@ #?(:cljs trie :clj (.matcher ^SegmentTrie (or trie (SegmentTrie.))))) +(defn scanner [compiled-tries] + "Returns a new compiled trie that does linear scan on the given compiled tries on [[lookup]]." + #?(:cljs (fn [path] (some (fn [trie] (lookup trie path)) 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) {})] diff --git a/perf-test/clj/reitit/opensensors_perf_test.clj b/perf-test/clj/reitit/opensensors_perf_test.clj index b53c2b09..24c282f0 100644 --- a/perf-test/clj/reitit/opensensors_perf_test.clj +++ b/perf-test/clj/reitit/opensensors_perf_test.clj @@ -13,7 +13,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])) + [io.pedestal.http.route.router :as pedestal] + [reitit.core :as r])) ;; ;; start repl with `lein perf repl` @@ -551,6 +552,7 @@ router (reitit/router routes) reitit-f #(reitit/match-by-path router (:uri %)) reitit-ring-f (ring/ring-handler (ring/router opensensors-routes)) + reitit-ring-linear-f (ring/ring-handler (ring/router opensensors-routes {:router r/linear-router})) reitit-ring-fast-f (ring/ring-handler (ring/router opensensors-routes) nil {:inject-router? false, :inject-match? false}) bidi-f #(bidi/match-route opensensors-bidi-routes (:uri %)) calfpath-macros-f opensensors-calfpath-macro-handler @@ -579,19 +581,23 @@ (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) ;; 2137ns (b! "calfpath-walker" calfpath-walker-f) ;; 4774ns - (b! "calfpath-unroll" calfpath-unroll-f) + #_(b! "calfpath-unroll" calfpath-unroll-f) ;; 2821ns (b! "pedestal" pedestal-f) ;; 4803ns - (b! "calfpath-macros" calfpath-macros-f) + #_(b! "calfpath-macros" calfpath-macros-f) ;; 11615ns (b! "compojure" compojure-f)