mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
commit
4e7963be91
23 changed files with 832 additions and 93 deletions
|
|
@ -4,6 +4,15 @@
|
|||
### `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
|
||||
|
||||
### `reitit-ring`
|
||||
|
||||
* new options `:inject-match?` and `:inject-router?` on `reitit.ring/ring-handler` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details.
|
||||
|
||||
### `reitit-http`
|
||||
|
||||
* new options `:inject-match?` and `:inject-router?` on `reitit.http/ring-handler` and `reitit.http/routing-interceptor` to optionally not to inject `Router` and `Match` into the request. See [performance guide](https://metosin.github.io/reitit/performance.html#faster!) for details.
|
||||
|
||||
## 0.2.10 (2018-12-30)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
# Performance
|
||||
|
||||
Reitit tries to be both great in features and be really, really fast. Originally the routing was ported from [Pedestal](http://pedestal.io/), but has been mostly rewritten.
|
||||
Besides having great features, goal of reitit is to be really, really fast. The routing was originally exported from Pedestal, but since rewritten.
|
||||
|
||||

|
||||
|
||||
|
|
@ -63,13 +63,13 @@ The routing sample taken from [bide](https://github.com/funcool/bide) README:
|
|||
(dotimes [_ 1000]
|
||||
(r/match-by-path routes "/auth/login")))
|
||||
|
||||
;; Execution time mean (per 1000): 530 µs -> 1.9M ops/sec
|
||||
;; Execution time mean (per 1000): 315 µs -> 3.2M ops/sec
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(r/match-by-path routes "/workspace/1/1")))
|
||||
```
|
||||
|
||||
Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 4-24x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
|
||||
Based on the [perf tests](https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj), the first (static path) lookup is 300-500x faster and the second (wildcard path) lookup is 6-40x faster that the other tested routing libs (Ataraxy, Bidi, Compojure and Pedestal).
|
||||
|
||||
But, the example is too simple for any real benchmark. Also, some of the libraries always match on the `:request-method` too and by doing so, do more work than just match by path. Compojure does most work also by invoking the handler.
|
||||
|
||||
|
|
@ -79,13 +79,13 @@ So, we need to test something more realistic.
|
|||
|
||||
To get better view on the real life routing performance, there is [test](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj) of a mid-size rest(ish) http api with 50+ routes, having a lot of path parameters. The route definitions are pulled off from the [OpenSensors](https://opensensors.io/) swagger definitions.
|
||||
|
||||
Thanks to the snappy new [segment-tree](https://github.com/metosin/reitit/blob/master/modules/reitit-core/src/reitit/segment.cljc) algorithm, `reitit-ring` is fastest here. Pedestal is also fast with it's [prefix-tree](https://en.wikipedia.org/wiki/Radix_tree) implementation.
|
||||
Thanks to the snappy [SegmentTrie](https://github.com/metosin/reitit/blob/master/modules/reitit-core/java-src/reitit/SegmentTrie.java) (a modification of [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree)), `reitit-ring` is fastest here. [Calfpath](https://github.com/kumarshantanu/calfpath) and [Pedestal](https://github.com/pedestal/pedestal) are also quite fast.
|
||||
|
||||

|
||||
|
||||
### CQRS apis
|
||||
|
||||
Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html)-style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste).
|
||||
Another real-life [test scenario](https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj) is a [CQRS](https://martinfowler.com/bliki/CQRS.html) style route tree, where all the paths are static, e.g. `/api/command/add-order`. The 300 route definitions are pulled out from [Lupapiste](https://github.com/lupapiste/lupapiste).
|
||||
|
||||
Both `reitit-ring` and Pedestal shine in this test, thanks to the fast lookup-routers. On average, they are **two** and on best case, **three orders of magnitude faster** than the other tested libs. Ataraxy failed this test on `Method code too large!` error.
|
||||
|
||||
|
|
@ -99,13 +99,42 @@ The reitit routing perf is measured to get an internal baseline to optimize agai
|
|||
|
||||
### Looking out of the box
|
||||
|
||||
A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is really awesome (if true).
|
||||
A quick poke to [routers in Go](https://github.com/julienschmidt/go-http-routing-benchmark) indicates that the reitit is only few times slower than the fastest routers in Go. Which is kinda awesome.
|
||||
|
||||
### Faster!
|
||||
|
||||
By default, `reitit.ring/ring-router`, `reitit.http/ring-router` and `reitit.http/routing-interceptor` inject both `Match` and `Router` into the request. You can remove the injections setting options `:inject-match?` and `:inject-router?` to `false`. This saves some tens of nanos (with the hw described above).
|
||||
|
||||
```clj
|
||||
(require '[reitit.ring :as ring])
|
||||
(require '[criterium.core :as cc])
|
||||
|
||||
(defn create [options]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
["/ping" (constantly {:status 200, :body "ok"})])
|
||||
(ring/create-default-handler)
|
||||
options))
|
||||
|
||||
;; 130ns
|
||||
(let [app (create nil)]
|
||||
(cc/quick-bench
|
||||
(app {:request-method :get, :uri "/ping"})))
|
||||
|
||||
;; 80ns
|
||||
(let [app (create {:inject-router? false, :inject-match? false})]
|
||||
(cc/quick-bench
|
||||
(app {:request-method :get, :uri "/ping"})))
|
||||
```
|
||||
|
||||
**NOTE**: Without `Router`, you can't to do [reverse routing](ring/reverse_routing.md) and without `Match` you can't write [dynamic extensions](ring/dynamic_extensions.md).
|
||||
|
||||
### Performance tips
|
||||
|
||||
Few things that have an effect on performance:
|
||||
|
||||
* Wildcard-routes are an order of magnitude slower than static routes
|
||||
* It's ok to mix non-wildcard and wildcard routes in a same routing tree as long as you don't disable the [conflict resolution](basics/route_conflicts.md) => if no conflicting routes are found, a `:mixed-router` can be created, which internally has a fast static path router and a separate wildcard-router. So, the static paths are still fast.
|
||||
* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md) & [route data](advanced/configuring_routers.md).
|
||||
* Conflicting routes are served with LinearRouter, which is the slowest implementation.
|
||||
* It's ok to mix non-wildcard, wildcard or even conflicting routes in a same routing tree. Reitit will create an hierarchy of routers to serve all the routes with best possible implementation.
|
||||
* Move computation from request processing time into creation time, using by compiling [middleware](ring/compiling_middleware.md), [interceptors](http/interceptors.md) and [route data](advanced/configuring_routers.md).
|
||||
* Unmounted middleware (or interceptor) is infinitely faster than a mounted one effectively doing nothing.
|
||||
|
|
|
|||
|
|
@ -51,9 +51,11 @@ Match contains `:result` compiled by the `ring-router`:
|
|||
|
||||
Given a `ring-router`, optional default-handler & options, `ring-handler` function will return a valid ring handler supporting both synchronous and [asynchronous](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html) request handling. The following options are available:
|
||||
|
||||
| key | description |
|
||||
| --------------|-------------|
|
||||
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
|
||||
| key | description |
|
||||
| ------------------|-------------|
|
||||
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
|
||||
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)
|
||||
|
||||
Simple Ring app:
|
||||
|
||||
|
|
|
|||
313
modules/reitit-core/java-src/reitit/SegmentTrie.java
Normal file
313
modules/reitit-core/java-src/reitit/SegmentTrie.java
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package reitit;
|
||||
|
||||
import clojure.lang.Keyword;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.*;
|
||||
|
||||
public class SegmentTrie {
|
||||
|
||||
public static ArrayList<String> split(final String path) {
|
||||
final ArrayList<String> segments = new ArrayList<>(4);
|
||||
final int size = path.length();
|
||||
int start = 1;
|
||||
for (int i = start; i < size; i++) {
|
||||
final char c = path.charAt(i);
|
||||
if (c == '/') {
|
||||
segments.add(path.substring(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
segments.add(path.substring(start, size));
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static String decode(String s) {
|
||||
try {
|
||||
if (s.contains("%")) {
|
||||
String _s = s;
|
||||
if (s.contains("+")) {
|
||||
_s = s.replace("+", "%2B");
|
||||
}
|
||||
return URLDecoder.decode(_s, "UTF-8");
|
||||
}
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public static class Match {
|
||||
public final Map<Keyword, String> params = new HashMap<>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, SegmentTrie> childs = new HashMap<>();
|
||||
private Map<Keyword, SegmentTrie> wilds = new HashMap<>();
|
||||
private Map<Keyword, SegmentTrie> catchAll = new HashMap<>();
|
||||
private Object data;
|
||||
|
||||
public SegmentTrie add(String path, Object data) {
|
||||
List<String> paths = split(path);
|
||||
SegmentTrie pointer = this;
|
||||
for (String p : paths) {
|
||||
if (p.startsWith(":")) {
|
||||
Keyword k = Keyword.intern(p.substring(1));
|
||||
SegmentTrie s = pointer.wilds.get(k);
|
||||
if (s == null) {
|
||||
s = new SegmentTrie();
|
||||
pointer.wilds.put(k, s);
|
||||
}
|
||||
pointer = s;
|
||||
} else if (p.startsWith("*")) {
|
||||
Keyword k = Keyword.intern(p.substring(1));
|
||||
SegmentTrie s = pointer.catchAll.get(k);
|
||||
if (s == null) {
|
||||
s = new SegmentTrie();
|
||||
pointer.catchAll.put(k, s);
|
||||
}
|
||||
pointer = s;
|
||||
break;
|
||||
} else {
|
||||
SegmentTrie s = pointer.childs.get(p);
|
||||
if (s == null) {
|
||||
s = new SegmentTrie();
|
||||
pointer.childs.put(p, s);
|
||||
}
|
||||
pointer = s;
|
||||
}
|
||||
}
|
||||
pointer.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Matcher staticMatcher() {
|
||||
if (childs.size() == 1) {
|
||||
return new StaticMatcher(childs.keySet().iterator().next(), childs.values().iterator().next().matcher());
|
||||
} else {
|
||||
Map<String, Matcher> m = new HashMap<>();
|
||||
for (Map.Entry<String, SegmentTrie> e : childs.entrySet()) {
|
||||
m.put(e.getKey(), e.getValue().matcher());
|
||||
}
|
||||
return new StaticMapMatcher(m);
|
||||
}
|
||||
}
|
||||
|
||||
public Matcher matcher() {
|
||||
Matcher m;
|
||||
if (!catchAll.isEmpty()) {
|
||||
m = new CatchAllMatcher(catchAll.keySet().iterator().next(), catchAll.values().iterator().next().data);
|
||||
if (data != null) {
|
||||
m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m));
|
||||
}
|
||||
} else if (!wilds.isEmpty()) {
|
||||
if (wilds.size() == 1 && data == null && childs.isEmpty()) {
|
||||
m = new WildMatcher(wilds.keySet().iterator().next(), wilds.values().iterator().next().matcher());
|
||||
} else {
|
||||
List<Matcher> matchers = new ArrayList<>();
|
||||
if (data != null) {
|
||||
matchers.add(new DataMatcher(data));
|
||||
}
|
||||
if (!childs.isEmpty()) {
|
||||
matchers.add(staticMatcher());
|
||||
}
|
||||
for (Map.Entry<Keyword, SegmentTrie> e : wilds.entrySet()) {
|
||||
matchers.add(new WildMatcher(e.getKey(), e.getValue().matcher()));
|
||||
}
|
||||
m = new LinearMatcher(matchers);
|
||||
}
|
||||
} else if (!childs.isEmpty()) {
|
||||
m = staticMatcher();
|
||||
if (data != null) {
|
||||
m = new LinearMatcher(Arrays.asList(new DataMatcher(data), m));
|
||||
}
|
||||
} else {
|
||||
return new DataMatcher(data);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
public interface Matcher {
|
||||
Match match(int i, List<String> segments, Match match);
|
||||
}
|
||||
|
||||
public static final class StaticMatcher implements Matcher {
|
||||
private final String segment;
|
||||
private final Matcher child;
|
||||
|
||||
StaticMatcher(String segment, Matcher child) {
|
||||
this.segment = segment;
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
if (i < segments.size() && segment.equals(segments.get(i))) {
|
||||
return child.match(i + 1, segments, match);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[\"" + segment + "\" " + child + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WildMatcher implements Matcher {
|
||||
private final Keyword parameter;
|
||||
private final Matcher child;
|
||||
|
||||
WildMatcher(Keyword parameter, Matcher child) {
|
||||
this.parameter = parameter;
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
if (i < segments.size() && !segments.get(i).isEmpty()) {
|
||||
final Match m = child.match(i + 1, segments, match);
|
||||
if (m != null) {
|
||||
m.params.put(parameter, decode(segments.get(i)));
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + parameter + " " + child + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CatchAllMatcher implements Matcher {
|
||||
private final Keyword parameter;
|
||||
private final Object data;
|
||||
|
||||
CatchAllMatcher(Keyword parameter, Object data) {
|
||||
this.parameter = parameter;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
if (i < segments.size()) {
|
||||
match.params.put(parameter, decode(String.join("/", segments.subList(i, segments.size()))));
|
||||
match.data = data;
|
||||
return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + parameter + " " + new DataMatcher(data) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StaticMapMatcher implements Matcher {
|
||||
private final Map<String, Matcher> map;
|
||||
|
||||
StaticMapMatcher(Map<String, Matcher> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
if (i < segments.size()) {
|
||||
final Matcher child = map.get(segments.get(i));
|
||||
if (child != null) {
|
||||
return child.match(i + 1, segments, match);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append("{");
|
||||
List<String> keys = new ArrayList<>(map.keySet());
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
String path = keys.get(i);
|
||||
Matcher value = map.get(path);
|
||||
b.append("\"").append(path).append("\" ").append(value);
|
||||
if (i < keys.size() - 1) {
|
||||
b.append(", ");
|
||||
}
|
||||
}
|
||||
b.append("}");
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class LinearMatcher implements Matcher {
|
||||
|
||||
private final List<Matcher> childs;
|
||||
|
||||
LinearMatcher(List<Matcher> childs) {
|
||||
this.childs = childs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
for (Matcher child : childs) {
|
||||
final Match m = child.match(i, segments, match);
|
||||
if (m != null) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return childs.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DataMatcher implements Matcher {
|
||||
private final Object data;
|
||||
|
||||
DataMatcher(Object data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Match match(int i, List<String> segments, Match match) {
|
||||
if (i == segments.size()) {
|
||||
match.data = data;
|
||||
return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return (data != null ? data.toString() : "nil");
|
||||
}
|
||||
}
|
||||
|
||||
public static Match lookup(Matcher matcher, String path) {
|
||||
return matcher.match(0, split(path), new Match());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
SegmentTrie trie = new SegmentTrie();
|
||||
trie.add("/repos/:owner/:repo/stargazers", 1);
|
||||
Matcher m = trie.matcher();
|
||||
System.err.println(m);
|
||||
System.err.println(m.getClass());
|
||||
System.out.println(lookup(m, "/repos/metosin/reitit/stargazers"));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,5 @@
|
|||
:plugins [[lein-parent "0.3.2"]]
|
||||
:parent-project {:path "../../project.clj"
|
||||
:inherit [:deploy-repositories :managed-dependencies]}
|
||||
:java-source-paths ["java-src"]
|
||||
:dependencies [[meta-merge]])
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@
|
|||
(if name (assoc nl name f) nl)]))
|
||||
[nil {}]
|
||||
compiled-routes)
|
||||
pl (segment/compile pl)
|
||||
lookup (impl/fast-map nl)
|
||||
routes (uncompile-routes compiled-routes)]
|
||||
^{:type ::router}
|
||||
|
|
@ -289,7 +290,7 @@
|
|||
(match-by-path [_ path]
|
||||
(if-let [match (segment/lookup pl path)]
|
||||
(-> (:data match)
|
||||
(assoc :path-params (impl/url-decode-coll (:path-params match)))
|
||||
(assoc :path-params (:path-params match))
|
||||
(assoc :path path))))
|
||||
(match-by-name [_ name]
|
||||
(if-let [match (impl/fast-get lookup name)]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
#?(:clj
|
||||
(:import (java.util.regex Pattern)
|
||||
(java.util HashMap Map)
|
||||
(java.net URLEncoder URLDecoder))))
|
||||
(java.net URLEncoder URLDecoder)
|
||||
(reitit SegmentTrie))))
|
||||
|
||||
(defn maybe-map-values
|
||||
"Applies a function to every value of a map, updates the value if not nil.
|
||||
|
|
@ -19,6 +20,16 @@
|
|||
coll
|
||||
coll))
|
||||
|
||||
(defn segments
|
||||
"Splits the path into sequence of segments, using `/` char. Assumes that the
|
||||
path starts with `/`, stripping the first empty segment. e.g.
|
||||
|
||||
(segments \"/a/b/c\") ; => (\"a\" \"b\" \"c\")
|
||||
(segments \"/a/) ; => (\"a\" \"\")"
|
||||
[path]
|
||||
#?(:clj (SegmentTrie/split ^String path)
|
||||
:cljs (rest (.split path #"/" 666))))
|
||||
|
||||
;;
|
||||
;; https://github.com/pedestal/pedestal/blob/master/route/src/io/pedestal/http/route/prefix_tree.clj
|
||||
;;
|
||||
|
|
@ -42,10 +53,6 @@
|
|||
(defn wild-or-catch-all-param? [x]
|
||||
(boolean (or (wild-param x) (catch-all-param x))))
|
||||
|
||||
(defn segments [path]
|
||||
#?(:clj (.split ^String path "/" 666)
|
||||
:cljs (.split path #"/" 666)))
|
||||
|
||||
(defn contains-wilds? [path]
|
||||
(boolean (some wild-or-catch-all-param? (segments path))))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
(ns reitit.segment
|
||||
(:refer-clojure :exclude [-lookup])
|
||||
(:refer-clojure :exclude [-lookup compile])
|
||||
(:require [reitit.impl :as impl]
|
||||
[clojure.string :as str]))
|
||||
[clojure.string :as str])
|
||||
#?(:clj (:import (reitit SegmentTrie SegmentTrie$Match))))
|
||||
|
||||
(defrecord Match [data path-params])
|
||||
|
||||
|
|
@ -43,14 +44,24 @@
|
|||
(if (and wilds? (not (str/blank? p))) (some #(-lookup (impl/fast-get children' %) ps (assoc path-params % p)) wilds))
|
||||
(if catch-all (-catch-all children' catch-all path-params p ps)))))))))
|
||||
|
||||
(defn insert [root path data]
|
||||
(-insert (or root (segment)) (impl/segments path) (map->Match {:data data})))
|
||||
;;
|
||||
;; public api
|
||||
;;
|
||||
|
||||
(defn create [paths]
|
||||
(reduce
|
||||
(fn [segment [p data]]
|
||||
(insert segment p data))
|
||||
nil paths))
|
||||
(defn insert
|
||||
"Returns a Segment Trie with path with data inserted into it. Creates the trie if `nil`."
|
||||
[trie path data]
|
||||
#?(:cljs (-insert (or trie (segment)) (impl/segments path) (map->Match {:data data}))
|
||||
:clj (.add (or ^SegmentTrie trie ^SegmentTrie (SegmentTrie.)) ^String path data)))
|
||||
|
||||
(defn lookup [segment path]
|
||||
(-lookup segment (impl/segments path) {}))
|
||||
(defn compile [trie]
|
||||
"Compiles the Trie so that [[lookup]] can be used."
|
||||
#?(:cljs trie
|
||||
:clj (.matcher ^SegmentTrie (or trie (SegmentTrie.)))))
|
||||
|
||||
(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) {})]
|
||||
(assoc match :path-params (impl/url-decode-coll (:path-params match))))
|
||||
:clj (if-let [match ^SegmentTrie$Match (SegmentTrie/lookup trie path)]
|
||||
(->Match (.data match) (clojure.lang.PersistentHashMap/create (.params match))))))
|
||||
|
|
|
|||
|
|
@ -73,12 +73,22 @@
|
|||
(r/router data opts))))
|
||||
|
||||
(defn routing-interceptor
|
||||
"A Pedestal-style routing interceptor that enqueus the interceptors into context."
|
||||
[router default-handler {:keys [interceptors executor]}]
|
||||
"Creates a Pedestal-style routing interceptor that enqueus the interceptors into context.
|
||||
Takes http-router, default ring-handler and and options map, with the following keys:
|
||||
|
||||
| key | description |
|
||||
| ------------------|-------------|
|
||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
|
||||
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||
[router default-handler {:keys [interceptors executor inject-match? inject-router?]
|
||||
:or {inject-match? true, inject-router? true}}]
|
||||
(let [default-handler (or default-handler (fn ([_])))
|
||||
default-interceptors (->> interceptors
|
||||
(map #(interceptor/into-interceptor % nil (r/options router))))
|
||||
default-queue (interceptor/queue executor default-interceptors)]
|
||||
default-queue (interceptor/queue executor default-interceptors)
|
||||
enrich-request (ring/create-enrich-request inject-match? inject-router?)]
|
||||
{:name ::router
|
||||
:enter (fn [{:keys [request] :as context}]
|
||||
(if-let [match (r/match-by-path router (:uri request))]
|
||||
|
|
@ -86,10 +96,7 @@
|
|||
path-params (:path-params match)
|
||||
endpoint (-> match :result method)
|
||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))
|
||||
request (enrich-request request path-params match router)
|
||||
context (assoc context :request request)
|
||||
queue (interceptor/queue executor (concat default-interceptors interceptors))]
|
||||
(interceptor/enqueue executor context queue))
|
||||
|
|
@ -103,13 +110,16 @@
|
|||
"Creates a ring-handler out of a http-router, optional default ring-handler
|
||||
and options map, with the following keys:
|
||||
|
||||
| key | description |
|
||||
| ----------------|-------------|
|
||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler"
|
||||
| key | description |
|
||||
| ------------------|-------------|
|
||||
| `:executor` | `reitit.interceptor.Executor` for the interceptor chain
|
||||
| `:interceptors` | Optional sequence of interceptors that are always run before any other interceptors, even for the default handler
|
||||
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||
([router opts]
|
||||
(ring-handler router nil opts))
|
||||
([router default-handler {:keys [executor interceptors]}]
|
||||
([router default-handler {:keys [executor interceptors inject-match? inject-router?]
|
||||
:or {inject-match? true, inject-router? true}}]
|
||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||
default-queue (->> [default-handler]
|
||||
(concat interceptors)
|
||||
|
|
@ -120,7 +130,9 @@
|
|||
(dissoc :data) ; data is already merged into routes
|
||||
(cond-> (seq interceptors)
|
||||
(update-in [:data :interceptors] (partial into (vec interceptors)))))
|
||||
router (reitit.http/router (r/routes router) router-opts)]
|
||||
router (reitit.http/router (r/routes router) router-opts)
|
||||
enrich-request (ring/create-enrich-request inject-match? inject-router?)
|
||||
enrich-default-request (ring/create-enrich-default-request inject-router?)]
|
||||
(with-meta
|
||||
(fn
|
||||
([request]
|
||||
|
|
@ -129,13 +141,10 @@
|
|||
path-params (:path-params match)
|
||||
endpoint (-> match :result method)
|
||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))]
|
||||
request (enrich-request request path-params match router)]
|
||||
(or (interceptor/execute executor interceptors request)
|
||||
(interceptor/execute executor default-queue request)))
|
||||
(interceptor/execute executor default-queue (impl/fast-assoc request ::r/router router))))
|
||||
(interceptor/execute executor default-queue (enrich-default-request request router))))
|
||||
([request respond raise]
|
||||
(let [default #(interceptor/execute executor default-queue % respond raise)]
|
||||
(if-let [match (r/match-by-path router (:uri request))]
|
||||
|
|
@ -143,10 +152,7 @@
|
|||
path-params (:path-params match)
|
||||
endpoint (-> match :result method)
|
||||
interceptors (or (:queue endpoint) (:interceptors endpoint))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))
|
||||
request (enrich-request request path-params match router)
|
||||
respond' (fn [response]
|
||||
(if response
|
||||
(respond response)
|
||||
|
|
@ -154,7 +160,7 @@
|
|||
(if interceptors
|
||||
(interceptor/execute executor interceptors request respond' raise)
|
||||
(default request)))
|
||||
(default (impl/fast-assoc request ::r/router router))))
|
||||
(default (enrich-default-request request router))))
|
||||
nil))
|
||||
{::r/router router}))))
|
||||
|
||||
|
|
|
|||
|
|
@ -226,20 +226,54 @@
|
|||
(not-found-handler request)))))]
|
||||
(create handler)))))
|
||||
|
||||
(defn create-enrich-request [inject-match? inject-router?]
|
||||
(cond
|
||||
(and inject-match? inject-router?)
|
||||
(fn enrich-request [request path-params match router]
|
||||
(-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router)))
|
||||
inject-router?
|
||||
(fn enrich-request [request path-params _ router]
|
||||
(-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/router router)))
|
||||
inject-match?
|
||||
(fn enrich-request [request path-params match _]
|
||||
(-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)))
|
||||
:else
|
||||
(fn enrich-request [request path-params _ _]
|
||||
(-> request
|
||||
(impl/fast-assoc :path-params path-params)))))
|
||||
|
||||
(defn create-enrich-default-request [inject-router?]
|
||||
(if inject-router?
|
||||
(fn enrich-request [request router]
|
||||
(impl/fast-assoc request ::r/router router))
|
||||
identity))
|
||||
|
||||
(defn ring-handler
|
||||
"Creates a ring-handler out of a router, optional default ring-handler
|
||||
and options map, with the following keys:
|
||||
|
||||
| key | description |
|
||||
| --------------|-------------|
|
||||
| `:middleware` | Optional sequence of middleware that wrap the ring-handler"
|
||||
| key | description |
|
||||
| ------------------|-------------|
|
||||
| `:middleware` | Optional sequence of middleware that wrap the ring-handler
|
||||
| `:inject-match?` | Boolean to inject `match` into request under `:reitit.core/match` key (default true)
|
||||
| `:inject-router?` | Boolean to inject `router` into request under `:reitit.core/router` key (default true)"
|
||||
([router]
|
||||
(ring-handler router nil))
|
||||
([router default-handler]
|
||||
(ring-handler router default-handler nil))
|
||||
([router default-handler {:keys [middleware]}]
|
||||
([router default-handler {:keys [middleware inject-match? inject-router?]
|
||||
:or {inject-match? true, inject-router? true}}]
|
||||
(let [default-handler (or default-handler (fn ([_]) ([_ respond _] (respond nil))))
|
||||
wrap (if middleware (partial middleware/chain middleware) identity)]
|
||||
wrap (if middleware (partial middleware/chain middleware) identity)
|
||||
enrich-request (create-enrich-request inject-match? inject-router?)
|
||||
enrich-default-request (create-enrich-default-request inject-router?)]
|
||||
(with-meta
|
||||
(wrap
|
||||
(fn
|
||||
|
|
@ -249,24 +283,18 @@
|
|||
path-params (:path-params match)
|
||||
result (:result match)
|
||||
handler (-> result method :handler (or default-handler))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))]
|
||||
request (enrich-request request path-params match router)]
|
||||
(or (handler request) (default-handler request)))
|
||||
(default-handler (impl/fast-assoc request ::r/router router))))
|
||||
(default-handler (enrich-default-request request router))))
|
||||
([request respond raise]
|
||||
(if-let [match (r/match-by-path router (:uri request))]
|
||||
(let [method (:request-method request)
|
||||
path-params (:path-params match)
|
||||
result (:result match)
|
||||
handler (-> result method :handler (or default-handler))
|
||||
request (-> request
|
||||
(impl/fast-assoc :path-params path-params)
|
||||
(impl/fast-assoc ::r/match match)
|
||||
(impl/fast-assoc ::r/router router))]
|
||||
request (enrich-request request path-params match router)]
|
||||
((routes handler default-handler) request respond raise))
|
||||
(default-handler (impl/fast-assoc request ::r/router router) respond raise))
|
||||
(default-handler (enrich-default-request request router) respond raise))
|
||||
nil)))
|
||||
{::r/router router}))))
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@
|
|||
(->> (impl/segments path)
|
||||
(map #(if (impl/wild-or-catch-all-param? %)
|
||||
(str "{" (subs % 1) "}") %))
|
||||
(str/join "/")))
|
||||
(str/join "/")
|
||||
(str "/")))
|
||||
|
||||
(defn create-swagger-handler []
|
||||
"Create a ring handler to emit swagger spec. Collects all routes from router which have
|
||||
|
|
|
|||
|
|
@ -89,10 +89,11 @@
|
|||
|
||||
;; 1600 µs
|
||||
(title "bidi")
|
||||
(assert (bidi/match-route bidi-routes "/auth/login"))
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(bidi/match-route bidi-routes "/auth/login")))
|
||||
(let [request "/auth/login"]
|
||||
(assert (bidi/match-route bidi-routes request))
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(bidi/match-route bidi-routes request))))
|
||||
|
||||
;; 1400 µs
|
||||
(title "ataraxy")
|
||||
|
|
@ -105,10 +106,10 @@
|
|||
;; 1000 µs
|
||||
(title "pedestal - map-tree => prefix-tree")
|
||||
(let [request {:path-info "/auth/login" :request-method :get}]
|
||||
(assert (pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get}))
|
||||
(assert (pedestal/find-route pedestal-router request))
|
||||
(cc/quick-bench
|
||||
(dotimes [_ 1000]
|
||||
(pedestal/find-route pedestal-router {:path-info "/auth/login" :request-method :get}))))
|
||||
(pedestal/find-route pedestal-router request))))
|
||||
|
||||
;; 1400 µs
|
||||
(title "compojure")
|
||||
|
|
@ -163,6 +164,7 @@
|
|||
;; 710 µs (3-18x)
|
||||
;; 530 µs (4-24x) -25% prefix-tree-router
|
||||
;; 710 µs (3-18x) segment-router
|
||||
;; 320 µs (6-40x) java-segment-router
|
||||
(title "reitit")
|
||||
(assert (reitit/match-by-path reitit-routes "/workspace/1/1"))
|
||||
(cc/quick-bench
|
||||
|
|
|
|||
120
perf-test/clj/reitit/calf_perf_test.clj
Normal file
120
perf-test/clj/reitit/calf_perf_test.clj
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
(ns reitit.calf-perf-test
|
||||
(:require [criterium.core :as cc]
|
||||
[reitit.perf-utils :refer :all]
|
||||
[ring.util.codec]
|
||||
[reitit.impl]
|
||||
[clojure.edn :as edn]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.core :as r])
|
||||
(:import (reitit SegmentTrie)))
|
||||
|
||||
;;
|
||||
;; 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))
|
||||
{:inject-match? false, :inject-router? false}))
|
||||
|
||||
(comment
|
||||
(let [request {:request-method :get
|
||||
:uri "/user/1234/profile/compact/"}]
|
||||
;; OLD: 1338ns
|
||||
;; NEW: 981ns
|
||||
;; JAVA: 805ns
|
||||
;; NO-INJECT: 704ns
|
||||
#_(cc/quick-bench
|
||||
(handler-reitit request))
|
||||
(handler-reitit request)))
|
||||
|
||||
|
||||
(comment
|
||||
;; 281ns
|
||||
(let [router (r/router [["/user/:id/profile/:type" ::1]
|
||||
["/user/:id/permissions" ::2]
|
||||
["/company/:cid/dept/:did" ::3]
|
||||
["/this/is/a/static/route" ::4]])]
|
||||
#_(cc/quick-bench
|
||||
(r/match-by-path router "/user/1234/profile/compact"))
|
||||
(r/match-by-path router "/user/1234/profile/compact")))
|
||||
|
||||
(comment
|
||||
(edn/read-string
|
||||
(str
|
||||
(.matcher
|
||||
(doto (SegmentTrie.)
|
||||
(.add "/user" 1)
|
||||
(.add "/user/:id" 2)
|
||||
(.add "/user/:id/orders" 3)
|
||||
(.add "/user/id/permissions" 4))))))
|
||||
|
||||
|
|
@ -296,7 +296,9 @@
|
|||
(def app
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
(reduce (partial add h) [] routes))))
|
||||
(reduce (partial add h) [] routes))
|
||||
(ring/create-default-handler)
|
||||
{:inject-match? false, :inject-router? false}))
|
||||
|
||||
(defrecord Req [uri request-method])
|
||||
|
||||
|
|
@ -313,6 +315,8 @@
|
|||
;; 40ns (httprouter)
|
||||
;; 140ns
|
||||
;; 120ns (faster decode params)
|
||||
;; 140µs (java-segment-router)
|
||||
;; 60ns (java-segment-router, no injects)
|
||||
(let [req (map->Req {:request-method :get, :uri "/user/repos"})]
|
||||
(title "static")
|
||||
(assert (= {:status 200, :body "/user/repos"} (app req)))
|
||||
|
|
@ -321,6 +325,9 @@
|
|||
;; 160ns (httprouter)
|
||||
;; 990ns
|
||||
;; 830ns (faster decode params)
|
||||
;; 560µs (java-segment-router)
|
||||
;; 490ns (java-segment-router, no injects)
|
||||
;; 440ns (java-segment-router, no injects, single-wild-optimization)
|
||||
(let [req (map->Req {:request-method :get, :uri "/repos/julienschmidt/httprouter/stargazers"})]
|
||||
(title "param")
|
||||
(assert (= {:status 200, :body "/repos/:owner/:repo/stargazers"} (app req)))
|
||||
|
|
@ -329,6 +336,9 @@
|
|||
;; 30µs (httprouter)
|
||||
;; 190µs
|
||||
;; 160µs (faster decode params)
|
||||
;; 120µs (java-segment-router)
|
||||
;; 100µs (java-segment-router, no injects)
|
||||
;; 90µs (java-segment-router, no injects, single-wild-optimization)
|
||||
(let [requests (mapv route->req routes)]
|
||||
(title "all")
|
||||
(cc/quick-bench
|
||||
|
|
|
|||
|
|
@ -185,9 +185,19 @@
|
|||
:c "1+1"
|
||||
:d "1"}))
|
||||
|
||||
(defn split! []
|
||||
|
||||
(suite "split")
|
||||
|
||||
;; 114ns (String/split)
|
||||
;; 82ns (SegmentTrie/split)
|
||||
(test "Splitting a String")
|
||||
(test! impl/segments "/olipa/kerran/:avaruus"))
|
||||
|
||||
(comment
|
||||
(url-decode!)
|
||||
(url-encode!)
|
||||
(form-decode!)
|
||||
(form-encode!)
|
||||
(url-encode-coll!))
|
||||
(url-encode-coll!)
|
||||
(split!))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
[bidi.bidi :as bidi]
|
||||
[ataraxy.core :as ataraxy]
|
||||
[compojure.core :refer [routes context ANY]]
|
||||
[calfpath.core :as cp]
|
||||
[calfpath.route :as cr]
|
||||
|
||||
[io.pedestal.http.route.definition.table :as table]
|
||||
[io.pedestal.http.route.map-tree :as map-tree]
|
||||
|
|
@ -366,6 +368,133 @@
|
|||
["/v1/users/:user-id/bookmarks" :get handler :route-name :test/route56]
|
||||
["/v1/orgs/:org-id/topics" :get handler :route-name :test/route57]])))
|
||||
|
||||
(defn opensensors-calfpath-macro-handler [request]
|
||||
(cp/->uri
|
||||
request
|
||||
"/v2/whoami" [] (cp/->get request (handler request) nil)
|
||||
"/v2/users/:user-id/datasets" [] (cp/->get request (handler request) nil)
|
||||
"/v2/public/projects/:project-id/datasets" [] (cp/->get request (handler request) nil)
|
||||
"/v1/public/topics/:topic" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/orgs/:org-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/search/topics/:term" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/invitations" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/devices/:batch/:type" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/topics" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/bookmarks/followers" [] (cp/->get request (handler request) nil)
|
||||
"/v2/datasets/:dataset-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/usage-stats" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/devices/:client-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/messages/user/:user-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/devices" [] (cp/->get request (handler request) nil)
|
||||
"/v1/public/users/:user-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/errors" [] (cp/->get request (handler request) nil)
|
||||
"/v1/public/orgs/:org-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/invitations" [] (cp/->get request (handler request) nil)
|
||||
"/v2/public/messages/dataset/bulk" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/devices/bulk" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/device-errors" [] (cp/->get request (handler request) nil)
|
||||
"/v2/login" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/usage-stats" [] (cp/->get request (handler request) nil)
|
||||
"/v2/users/:user-id/devices" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/claim-device/:client-id" [] (cp/->get request (handler request) nil)
|
||||
"/v2/public/projects/:project-id" [] (cp/->get request (handler request) nil)
|
||||
"/v2/public/datasets/:dataset-id" [] (cp/->get request (handler request) nil)
|
||||
"/v2/users/:user-id/topics/bulk" [] (cp/->get request (handler request) nil)
|
||||
"/v1/messages/device/:client-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/owned-orgs" [] (cp/->get request (handler request) nil)
|
||||
"/v1/topics/:topic" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/bookmark/:topic" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/members/:user-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/devices/:client-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/devices" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/members" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/members/invitation-data/:user-id" [] (cp/->get request (handler request) nil)
|
||||
"/v2/orgs/:org-id/topics" [] (cp/->get request (handler request) nil)
|
||||
"/v1/whoami" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/api-key" [] (cp/->get request (handler request) nil)
|
||||
"/v2/schemas" [] (cp/->get request (handler request) nil)
|
||||
"/v2/users/:user-id/topics" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/confirm-membership/:token" [] (cp/->get request (handler request) nil)
|
||||
"/v2/topics/:topic" [] (cp/->get request (handler request) nil)
|
||||
"/v1/messages/topic/:topic" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/devices/:client-id/reset-password" [] (cp/->get request (handler request) nil)
|
||||
"/v2/topics" [] (cp/->get request (handler request) nil)
|
||||
"/v1/login" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/orgs" [] (cp/->get request (handler request) nil)
|
||||
"/v2/public/messages/dataset/:dataset-id" [] (cp/->get request (handler request) nil)
|
||||
"/v1/topics" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs" [] (cp/->get request (handler request) nil)
|
||||
"/v1/users/:user-id/bookmarks" [] (cp/->get request (handler request) nil)
|
||||
"/v1/orgs/:org-id/topics" [] (cp/->get request (handler request) nil)
|
||||
nil))
|
||||
|
||||
(def opensensors-calfpath-routes
|
||||
[{:uri "/v2/whoami" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/users/:user-id/datasets" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/public/projects/:project-id/datasets" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/public/topics/:topic" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/orgs/:org-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/search/topics/:term" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/invitations" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/devices/:batch/:type" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/topics" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/bookmarks/followers" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/datasets/:dataset-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/usage-stats" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/devices/:client-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/messages/user/:user-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/devices" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/public/users/:user-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/errors" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/public/orgs/:org-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/invitations" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/public/messages/dataset/bulk" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/devices/bulk" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/device-errors" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/login" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/usage-stats" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/users/:user-id/devices" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/claim-device/:client-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/public/projects/:project-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/public/datasets/:dataset-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/users/:user-id/topics/bulk" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/messages/device/:client-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/owned-orgs" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/topics/:topic" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/bookmark/:topic" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/members/:user-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/devices/:client-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/devices" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/members" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/members/invitation-data/:user-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/whoami" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/api-key" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/schemas" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/users/:user-id/topics" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/confirm-membership/:token" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/topics/:topic" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/messages/topic/:topic" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/devices/:client-id/reset-password" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/topics" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/login" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/orgs" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v2/public/messages/dataset/:dataset-id" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/topics" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/users/:user-id/bookmarks" :nested [{:method :get, :handler handler}]}
|
||||
{:uri "/v1/orgs/:org-id/topics" :nested [{:method :get, :handler handler}]}])
|
||||
|
||||
(def opensensors-calfpath-walker-handler
|
||||
(partial cr/dispatch (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false})))
|
||||
|
||||
(def opensensors-calfpath-unroll-handler
|
||||
(cr/make-dispatcher (cr/compile-routes opensensors-calfpath-routes {:show-uris-400? false})))
|
||||
|
||||
(comment
|
||||
(pedestal/find-route
|
||||
(map-tree/router
|
||||
|
|
@ -422,7 +551,11 @@
|
|||
router (reitit/router routes)
|
||||
reitit-f #(reitit/match-by-path router (:uri %))
|
||||
reitit-ring-f (ring/ring-handler (ring/router opensensors-routes))
|
||||
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
|
||||
calfpath-walker-f opensensors-calfpath-walker-handler
|
||||
calfpath-unroll-f opensensors-calfpath-unroll-handler
|
||||
ataraxy-f (partial ataraxy/matches opensensors-ataraxy-routes)
|
||||
compojure-f opensensors-compojure-routes
|
||||
pedestal-f (partial pedestal/find-route opensensors-pedestal-routes)
|
||||
|
|
@ -432,6 +565,7 @@
|
|||
;; 2065ns
|
||||
;; 662ns (prefix-tree-router)
|
||||
;; 567ns (segment-router)
|
||||
;; 326ns (java-segment-router)
|
||||
(b! "reitit" reitit-f)
|
||||
|
||||
;; 2845ns
|
||||
|
|
@ -441,11 +575,24 @@
|
|||
;; 702ns (before path-parameters)
|
||||
;; 806ns (decode path-parameters)
|
||||
;; 735ns (maybe-map-values)
|
||||
;; 474ns (java-segment-router)
|
||||
(b! "reitit-ring" reitit-ring-f)
|
||||
|
||||
;; 385ns (java-segment-router, no injects)
|
||||
(b! "reitit-ring-fast" reitit-ring-fast-f)
|
||||
|
||||
;; 2137ns
|
||||
(b! "calfpath-walker" calfpath-walker-f)
|
||||
|
||||
;; 4774ns
|
||||
(b! "calfpath-unroll" calfpath-unroll-f)
|
||||
|
||||
;; 2821ns
|
||||
(b! "pedestal" pedestal-f)
|
||||
|
||||
;; 4803ns
|
||||
(b! "calfpath-macros" calfpath-macros-f)
|
||||
|
||||
;; 11615ns
|
||||
(b! "compojure" compojure-f)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
(:require [clojure.test :refer :all]
|
||||
[io.pedestal.http.route.prefix-tree :as p]
|
||||
[reitit.segment :as segment]
|
||||
[criterium.core :as cc]))
|
||||
[criterium.core :as cc])
|
||||
(:import (reitit SegmentTrie)))
|
||||
|
||||
;;
|
||||
;; testing
|
||||
|
|
@ -69,14 +70,13 @@
|
|||
(p/insert acc p d))
|
||||
nil routes))
|
||||
|
||||
#_(def reitit-tree
|
||||
(def matcher
|
||||
(.matcher
|
||||
^SegmentTrie
|
||||
(reduce
|
||||
(fn [acc [p d]]
|
||||
(trie/insert acc p d))
|
||||
nil routes))
|
||||
|
||||
(def reitit-segment
|
||||
(segment/create routes))
|
||||
(segment/insert acc p d))
|
||||
nil routes)))
|
||||
|
||||
(defn bench! []
|
||||
|
||||
|
|
@ -108,8 +108,9 @@
|
|||
;; 1.0µs (Match records)
|
||||
;; 0.63µs (Single sweep path paraµs)
|
||||
;; 0.51µs (Cleanup)
|
||||
;; 0.30µs (Java)
|
||||
(cc/quick-bench
|
||||
(segment/lookup reitit-segment "/v1/orgs/1/topics")))
|
||||
(segment/lookup matcher "/v1/orgs/1/topics")))
|
||||
|
||||
(comment
|
||||
(bench!))
|
||||
|
|
@ -117,4 +118,4 @@
|
|||
(comment
|
||||
(p/lookup pedestal-tree "/v1/orgs/1/topics")
|
||||
#_(trie/lookup reitit-tree "/v1/orgs/1/topics" {})
|
||||
(segment/lookup reitit-segment "/v1/orgs/1/topics"))
|
||||
(segment/lookup matcher "/v1/orgs/1/topics"))
|
||||
|
|
|
|||
|
|
@ -18,21 +18,31 @@
|
|||
;; Memory: 16 GB
|
||||
;;
|
||||
|
||||
(def app
|
||||
(defn create-app [options]
|
||||
(ring/ring-handler
|
||||
(ring/router
|
||||
[["/auth/login" identity]
|
||||
["/auth/recovery/token/:token" identity]
|
||||
["/workspace/:project/:page" identity]])))
|
||||
["/workspace/:project/:page" identity]])
|
||||
(ring/create-default-handler)
|
||||
options))
|
||||
|
||||
(comment
|
||||
(let [request {:request-method :post, :uri "/auth/login"}]
|
||||
(defn bench-app []
|
||||
(let [request {:request-method :post, :uri "/auth/login"}
|
||||
app1 (create-app nil)
|
||||
app2 (create-app {:inject-match? false, :inject-router? false})]
|
||||
;; 192ns (initial)
|
||||
;; 163ns (always assoc path params)
|
||||
;; 132ns (expand methods)
|
||||
;; 111ns (java-segment-router)
|
||||
(cc/quick-bench
|
||||
(app request))
|
||||
(app1 request))
|
||||
|
||||
;; 113ns (don't inject router)
|
||||
;; 89ns (don't inject router & match)
|
||||
))
|
||||
;; 77ns (java-segment-router)
|
||||
(cc/quick-bench
|
||||
(app2 request))))
|
||||
|
||||
(comment
|
||||
(bench-app))
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@
|
|||
"modules/reitit-sieppari/src"
|
||||
"modules/reitit-pedestal/src"]
|
||||
|
||||
:java-source-paths ["modules/reitit-core/java-src"]
|
||||
|
||||
:dependencies [[org.clojure/clojure "1.10.0"]
|
||||
[org.clojure/clojurescript "1.10.439"]
|
||||
|
||||
|
|
@ -99,6 +101,7 @@
|
|||
[ikitommi/immutant-web "3.0.0-alpha1"]
|
||||
[io.pedestal/pedestal.service "0.5.5"]
|
||||
[io.pedestal/pedestal.jetty "0.5.5"]
|
||||
[calfpath "0.7.1"]
|
||||
[org.clojure/core.async "0.4.490"]
|
||||
[manifold "0.1.8"]
|
||||
[funcool/promesa "1.9.0"]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
(is (= [["/api/ipa/:size" {:name ::beer}]]
|
||||
(r/routes router)))
|
||||
(is (map? (r/options router)))
|
||||
(is (= nil
|
||||
(r/match-by-path router "/api")))
|
||||
(is (= (r/map->Match
|
||||
{:template "/api/ipa/:size"
|
||||
:data {:name ::beer}
|
||||
|
|
@ -72,8 +74,9 @@
|
|||
["/abba/1" ::abba2]
|
||||
["/:jabba/2" ::jabba2]
|
||||
["/:abba/:dabba/doo" ::doo]
|
||||
["/abba/dabba/boo/baa" ::baa]
|
||||
["/abba/:dabba/boo" ::boo]
|
||||
["/:jabba/:dabba/:doo/*foo" ::wild]]
|
||||
["/:jabba/:dabba/:doo/:daa/*foo" ::wild]]
|
||||
{:router r})
|
||||
matches #(-> router (r/match-by-path %) :data :name)]
|
||||
(is (= ::abba (matches "/abba")))
|
||||
|
|
@ -81,6 +84,8 @@
|
|||
(is (= ::jabba2 (matches "/abba/2")))
|
||||
(is (= ::doo (matches "/abba/1/doo")))
|
||||
(is (= ::boo (matches "/abba/1/boo")))
|
||||
(is (= ::baa (matches "/abba/dabba/boo/baa")))
|
||||
(is (= ::boo (matches "/abba/dabba/boo")))
|
||||
(is (= ::wild (matches "/olipa/kerran/avaruus/vaan/ei/toista/kertaa")))))
|
||||
|
||||
(testing "empty path segments"
|
||||
|
|
@ -108,6 +113,8 @@
|
|||
(is (= [["/api/ipa/large" {:name ::beer}]]
|
||||
(r/routes router)))
|
||||
(is (map? (r/options router)))
|
||||
(is (= nil
|
||||
(r/match-by-path router "/api")))
|
||||
(is (= (r/map->Match
|
||||
{:template "/api/ipa/large"
|
||||
:data {:name ::beer}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
[reitit.impl :as impl]))
|
||||
|
||||
(deftest segments-test
|
||||
(is (= ["" "api" "ipa" "beer" "craft" "bisse"]
|
||||
(is (= ["api" "ipa" "beer" "craft" "bisse"]
|
||||
(into [] (impl/segments "/api/ipa/beer/craft/bisse"))))
|
||||
(is (= ["" "a" "" "b" "" "c" ""]
|
||||
(is (= ["a" "" "b" "" "c" ""]
|
||||
(into [] (impl/segments "/a//b//c/")))))
|
||||
|
||||
(deftest strip-nils-test
|
||||
|
|
|
|||
21
test/cljc/reitit/segment_test.cljc
Normal file
21
test/cljc/reitit/segment_test.cljc
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
(ns reitit.segment-test
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.segment :as s]))
|
||||
|
||||
(deftest tests
|
||||
(is (= (s/->Match {:a 1} {})
|
||||
(-> (s/insert nil "/foo" {:a 1})
|
||||
(s/compile)
|
||||
(s/lookup "/foo"))))
|
||||
|
||||
(is (= (s/->Match {:a 1} {})
|
||||
(-> (s/insert nil "/foo" {:a 1})
|
||||
(s/insert "/foo/*bar" {:b 1})
|
||||
(s/compile)
|
||||
(s/lookup "/foo"))))
|
||||
|
||||
(is (= (s/->Match {:b 1} {:bar "bar"})
|
||||
(-> (s/insert nil "/foo" {:a 1})
|
||||
(s/insert "/foo/*bar" {:b 1})
|
||||
(s/compile)
|
||||
(s/lookup "/foo/bar")))))
|
||||
Loading…
Reference in a new issue