diff --git a/modules/reitit-core/java-src/reitit/Util.java b/modules/reitit-core/java-src/reitit/Util.java new file mode 100644 index 00000000..767731c4 --- /dev/null +++ b/modules/reitit-core/java-src/reitit/Util.java @@ -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 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 pathParams = new HashMap(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 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)); + } +} diff --git a/perf-test/clj/reitit/calf_perf_test.clj b/perf-test/clj/reitit/calf_perf_test.clj new file mode 100644 index 00000000..9b923a46 --- /dev/null +++ b/perf-test/clj/reitit/calf_perf_test.clj @@ -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/")))