This commit is contained in:
Tommi Reiman 2019-01-08 10:03:35 +02:00
parent 2a1fea2ccb
commit ae2337621f
2 changed files with 314 additions and 0 deletions

View file

@ -0,0 +1,96 @@
package reitit;
import clojure.lang.Keyword;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Util {
public static Map<?, String> matchURI(String uri, List<?> patternTokens) {
final MatchResult result = matchURI(uri, 0, patternTokens, false);
return result == null ? null : result.getParams();
}
public static MatchResult matchURI(String uri, int beginIndex, List<?> patternTokens) {
return matchURI(uri, beginIndex, patternTokens, false);
}
/**
* Match a URI against URI-pattern tokens and return match result on successful match, {@code null} otherwise.
* When argument {@code attemptPartialMatch} is {@code true}, both full and partial match are attempted without any
* performance penalty. When argument {@code attemptPartialMatch} is {@code false}, only a full match is attempted.
*
* @param uri the URI string to match
* @param beginIndex beginning index in the URI string to match
* @param patternTokens URI pattern tokens to match the URI against
* @param attemptPartialMatch whether attempt partial match when full match is not possible
* @return a match result on successful match, {@literal null} otherwise
*/
public static MatchResult matchURI(String uri, int beginIndex, List<?> patternTokens, boolean attemptPartialMatch) {
if (beginIndex == MatchResult.FULL_MATCH_INDEX) { // if already a full-match then no need to match further
return MatchResult.NO_MATCH;
}
final int tokenCount = patternTokens.size();
// if length==1, then token must be string (static URI path)
if (tokenCount == 1) {
final String staticPath = (String) patternTokens.get(0);
if (uri.startsWith(staticPath, beginIndex)) { // URI begins with the path, so at least partial match exists
if ((uri.length() - beginIndex) == staticPath.length()) { // if full match exists, then return as such
return MatchResult.FULL_MATCH_NO_PARAMS;
}
return attemptPartialMatch ? MatchResult.partialMatch(staticPath.length()) : MatchResult.NO_MATCH;
} else {
return MatchResult.NO_MATCH;
}
}
final int uriLength = uri.length();
final Map<Object, String> pathParams = new HashMap<Object, String>(tokenCount);
int uriIndex = beginIndex;
OUTER:
for (final Object token : patternTokens) {
if (uriIndex >= uriLength) {
return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH;
}
if (token instanceof String) {
final String tokenStr = (String) token;
if (uri.startsWith(tokenStr, uriIndex)) {
uriIndex += tokenStr.length(); // now i==n if last string token
} else { // 'string token mismatch' implies no match
return MatchResult.NO_MATCH;
}
} else {
final StringBuilder sb = new StringBuilder();
for (int j = uriIndex; j < uriLength; j++) { // capture param chars in one pass
final char ch = uri.charAt(j);
if (ch == '/') { // separator implies we got param value, now continue
pathParams.put(token, sb.toString());
uriIndex = j;
continue OUTER;
} else {
sb.append(ch);
}
}
// 'separator not found' implies URI has ended
pathParams.put(token, sb.toString());
uriIndex = uriLength;
}
}
if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match
return attemptPartialMatch ? MatchResult.partialMatch(pathParams, uriIndex) : MatchResult.NO_MATCH;
}
return MatchResult.fullMatch(pathParams);
}
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
list.add("/user/");
list.add(Keyword.intern("userId"));
list.add("/profile/");
list.add(Keyword.intern("type"));
list.add("/");
System.out.println(Util.matchURI("/user/1234/profile/compact/", list));
}
}

View file

@ -0,0 +1,218 @@
(ns reitit.calf-perf-test
(:require [criterium.core :as cc]
[reitit.perf-utils :refer :all]
[ring.util.codec]
[reitit.impl]
[reitit.segment :as segment]
[reitit.impl :as impl]
[reitit.ring :as ring]
[reitit.core :as r])
(:import (reitit Trie)))
;;
;; start repl with `lein perf repl`
;; perf measured with the following setup:
;;
;; Model Name: MacBook Pro
;; Model Identifier: MacBookPro11,3
;; Processor Name: Intel Core i7
;; Processor Speed: 2,5 GHz
;; Number of Processors: 1
;; Total Number of Cores: 4
;; L2 Cache (per Core): 256 KB
;; L3 Cache: 6 MB
;; Memory: 16 GB
;;
(defn test! [f input]
(do
(println "\u001B[33m")
(println (pr-str input) "=>" (pr-str (f input)))
(println "\u001B[0m")
(cc/quick-bench (f input))))
(defn h11 [id type] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".11." type)})
(defn h12 [id type] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".12." type)})
(defn h1x [] {:status 405
:headers {"Allow" "GET, PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Supported methods are: GET, PUT"})
(defn h21 [id] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".21")})
(defn h22 [id] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str id ".22")})
(defn h2x [] {:status 405
:headers {"Allow" "GET, PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Supported methods are: GET, PUT"})
(defn h30 [cid did] {:status 200
:headers {"Content-Type" "text/plain"}
:body (str cid ".3." did)})
(defn h3x [] {:status 405
:headers {"Allow" "PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Only PUT is supported."})
(defn h40 [] {:status 200
:headers {"Content-Type" "text/plain"}
:body "4"})
(defn h4x [] {:status 405
:headers {"Allow" "PUT"
"Content-Type" "text/plain"}
:body "405 Method not supported. Only PUT is supported."})
(defn hxx [] {:status 400
:headers {"Content-Type" "text/plain"}
:body "400 Bad request. URI does not match any available uri-template."})
(def handler-reitit
(ring/ring-handler
(ring/router
[["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type))
:put (fn [{{:keys [id type]} :path-params}] (h12 id type))
:handler (fn [_] (h1x))}]
#_["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id))
:put (fn [{{:keys [id]} :path-params}] (h22 id))
:handler (fn [_] (h2x))}]
#_["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did))
:handler (fn [_] (h3x))}]
#_["/this/is/a/static/route" {:put (fn [_] (h40))
:handler (fn [_] (h4x))}]])
(fn [_] (hxx))))
#_(let [request {:request-method :put
:uri "/this/is/a/static/route"}]
(handler-reitit request)
(cc/quick-bench
(handler-reitit request)))
(let [request {:request-method :get
:uri "/user/1234/profile/compact/"}]
(handler-reitit request)
;; OLD: 1338ns
;; NEW: 981ns
#_(cc/quick-bench
(handler-reitit request)))
(comment
;; 849ns (clojure, original)
;; 599ns (java, initial)
;; 810ns (linear)
(let [router (r/router ["/user/:id/profile/:type"])]
(cc/quick-bench
(r/match-by-path router "/user/1234/profile/compact")))
;; 131ns
(let [route ["/user/" :id "/profile/" :type "/"]]
(cc/quick-bench
(Util/matchURI "/user/1234/profile/compact/" route)))
;; 728ns
(cc/quick-bench
(r/match-by-path ring/ROUTER (:uri ring/REQUEST))))
(set! *warn-on-reflection* true)
(comment
(let [request {:request-method :get
:uri "/user/1234/profile/compact/"}]
(time
(dotimes [_ 1000]
(handler-reitit request)))))
(import '[reitit Util])
#_(cc/quick-bench
(Trie/split "/this/is/a/static/route"))
(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])
(import '[reitit Segment2])
(def paths ["kikka" "kukka" "kakka" "abba" "jabba" "1" "2" "3" "4"])
(def a (Segment2/createArray paths))
(def h (Segment2/createHash paths))
(set! *warn-on-reflection* true)
(comment
(let [segment (segment/create
[["/user/:id/profile/:type/" 1]
["/user/:id/permissions/" 2]
["/company/:cid/dept/:did/" 3]
["/this/is/a/static/route" 4]])]
(segment/lookup segment "/user/1/profile/compat/")
;; OLD: 602ns
;; NEW: 472ns
(cc/quick-bench
(segment/lookup segment "/user/1/profile/compat/"))
;; OLD: 454ns
;; NEW: 372ns
(cc/quick-bench
(segment/lookup segment "/user/1/permissions/"))))
#_(cc/quick-bench
(Trie/split "/user/1/profile/compat"))
#_(Trie/split "/user/1/profile/compat")
#_(cc/quick-bench
(Segment2/hashLookup h "abba"))
(comment
(cc/quick-bench
(dotimes [_ 1000]
;; 7ns
(Segment2/arrayLookup a "abba")))
(cc/quick-bench
(dotimes [_ 1000]
;; 3ns
(Segment2/hashLookup h "abba"))))
(comment
(time
(dotimes [_ 1000]
(Util/matchURI "/user/1234/profile/compact/" ["/user/" :id "/profile/" :type "/"])))
(time
(let [s (s/create [["/user/:id/profile/:type/" 1]])]
(dotimes [_ 1000]
(s/lookup s "/user/1234/profile/compact/"))))
(let [m {"/abba" 1}]
(time
(dotimes [_ 1000]
(get m "/abba"))))
(time
(dotimes [_ 1000]
(Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false)))
;; 124ns
(cc/quick-bench
(Util/matchURI "/user/1234/profile/compact/" 0 ["/user/" :id "/profile/" :type "/"] false))
;; 166ns
(cc/quick-bench
(impl/segments "/user/1234/profile/compact/"))
;; 597ns
(let [s (s/create [["/user/:id/profile/:type/" 1]])]
(cc/quick-bench
(s/lookup s "/user/1234/profile/compact/")))
(let [s (s/create [["/user/:id/profile/:type/" 1]])]
(s/lookup s "/user/1234/profile/compact/")))