This commit is contained in:
Tommi Reiman 2019-01-26 16:03:44 +02:00
parent 25287e0df7
commit f2d131a897
9 changed files with 469 additions and 20 deletions

View file

@ -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 []

View file

@ -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<Object> params = new ArrayList<>();
public Object data;
@Override
public String toString() {
Map<Object, Object> 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<Matcher> childs) {
return new LinearMatcher(childs);
}
static final class LinearMatcher implements Matcher {
private final Matcher[] childs;
private final int size;
LinearMatcher(List<Matcher> 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"));
}
}

View file

@ -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]])

View file

@ -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))))

View file

@ -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]]]

View file

@ -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")))))

View file

@ -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!))

View file

@ -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"))

View file

@ -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"]