mirror of
https://github.com/metosin/reitit.git
synced 2025-12-20 17:41:11 +00:00
706 lines
24 KiB
HTML
706 lines
24 KiB
HTML
|
|
<!DOCTYPE HTML>
|
|
<html lang="" >
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
|
<title>Performance · GitBook</title>
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<meta name="description" content="">
|
|
<meta name="generator" content="GitBook 3.2.3">
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="gitbook/style.css">
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="gitbook/gitbook-plugin-highlight/website.css">
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="gitbook/gitbook-plugin-search/search.css">
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="gitbook/gitbook-plugin-fontsettings/website.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<meta name="HandheldFriendly" content="true"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="gitbook/images/apple-touch-icon-precomposed-152.png">
|
|
<link rel="shortcut icon" href="gitbook/images/favicon.ico" type="image/x-icon">
|
|
|
|
|
|
<link rel="next" href="faq.html" />
|
|
|
|
|
|
<link rel="prev" href="ring/compiling_middleware.html" />
|
|
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<div class="book">
|
|
<div class="book-summary">
|
|
|
|
|
|
<div id="book-search-input" role="search">
|
|
<input type="text" placeholder="Type to search" />
|
|
</div>
|
|
|
|
|
|
<nav role="navigation">
|
|
|
|
|
|
|
|
<ul class="summary">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<li class="chapter " data-level="1.1" data-path="./">
|
|
|
|
<a href="./">
|
|
|
|
|
|
Introduction
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2" data-path="basics/">
|
|
|
|
<a href="basics/">
|
|
|
|
|
|
Basics
|
|
|
|
</a>
|
|
|
|
|
|
|
|
<ul class="articles">
|
|
|
|
|
|
<li class="chapter " data-level="1.2.1" data-path="basics/route_syntax.html">
|
|
|
|
<a href="basics/route_syntax.html">
|
|
|
|
|
|
Route Syntax
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.2" data-path="basics/router.html">
|
|
|
|
<a href="basics/router.html">
|
|
|
|
|
|
Router
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.3" data-path="basics/path_based_routing.html">
|
|
|
|
<a href="basics/path_based_routing.html">
|
|
|
|
|
|
Path-based Routing
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.4" data-path="basics/name_based_routing.html">
|
|
|
|
<a href="basics/name_based_routing.html">
|
|
|
|
|
|
Name-based Routing
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.5" data-path="basics/route_data.html">
|
|
|
|
<a href="basics/route_data.html">
|
|
|
|
|
|
Route Data
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.6" data-path="basics/route_data_validation.html">
|
|
|
|
<a href="basics/route_data_validation.html">
|
|
|
|
|
|
Route Data Validation
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.2.7" data-path="basics/route_conflicts.html">
|
|
|
|
<a href="basics/route_conflicts.html">
|
|
|
|
|
|
Route Conflicts
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.3" data-path="coercion/">
|
|
|
|
<a href="coercion/">
|
|
|
|
|
|
Coercion
|
|
|
|
</a>
|
|
|
|
|
|
|
|
<ul class="articles">
|
|
|
|
|
|
<li class="chapter " data-level="1.3.1" data-path="coercion/coercion.html">
|
|
|
|
<a href="coercion/coercion.html">
|
|
|
|
|
|
Coercion Explained
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.3.2" data-path="coercion/schema_coercion.html">
|
|
|
|
<a href="coercion/schema_coercion.html">
|
|
|
|
|
|
Plumatic Schema
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.3.3" data-path="coercion/clojure_spec_coercion.html">
|
|
|
|
<a href="coercion/clojure_spec_coercion.html">
|
|
|
|
|
|
Clojure.spec
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.3.4" data-path="coercion/data_spec_coercion.html">
|
|
|
|
<a href="coercion/data_spec_coercion.html">
|
|
|
|
|
|
Data-specs
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.4" data-path="advanced/">
|
|
|
|
<a href="advanced/">
|
|
|
|
|
|
Advanced
|
|
|
|
</a>
|
|
|
|
|
|
|
|
<ul class="articles">
|
|
|
|
|
|
<li class="chapter " data-level="1.4.1" data-path="advanced/configuring_routers.html">
|
|
|
|
<a href="advanced/configuring_routers.html">
|
|
|
|
|
|
Configuring Routers
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.4.2" data-path="advanced/different_routers.html">
|
|
|
|
<a href="advanced/different_routers.html">
|
|
|
|
|
|
Different Routers
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.4.3" data-path="advanced/route_validation.html">
|
|
|
|
<a href="advanced/route_validation.html">
|
|
|
|
|
|
Route Validation
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.4.4" data-path="advanced/interceptors.html">
|
|
|
|
<a href="advanced/interceptors.html">
|
|
|
|
|
|
Interceptors
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5" data-path="ring/">
|
|
|
|
<a href="ring/">
|
|
|
|
|
|
Ring
|
|
|
|
</a>
|
|
|
|
|
|
|
|
<ul class="articles">
|
|
|
|
|
|
<li class="chapter " data-level="1.5.1" data-path="ring/ring.html">
|
|
|
|
<a href="ring/ring.html">
|
|
|
|
|
|
Ring-router
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5.2" data-path="ring/dynamic_extensions.html">
|
|
|
|
<a href="ring/dynamic_extensions.html">
|
|
|
|
|
|
Dynamic Extensions
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5.3" data-path="ring/data_driven_middleware.html">
|
|
|
|
<a href="ring/data_driven_middleware.html">
|
|
|
|
|
|
Data-driven Middleware
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5.4" data-path="ring/coercion.html">
|
|
|
|
<a href="ring/coercion.html">
|
|
|
|
|
|
Pluggable Coercion
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5.5" data-path="ring/route_data_validation.html">
|
|
|
|
<a href="ring/route_data_validation.html">
|
|
|
|
|
|
Route Data Validation
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.5.6" data-path="ring/compiling_middleware.html">
|
|
|
|
<a href="ring/compiling_middleware.html">
|
|
|
|
|
|
Compiling Middleware
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
</li>
|
|
|
|
<li class="chapter active" data-level="1.6" data-path="performance.html">
|
|
|
|
<a href="performance.html">
|
|
|
|
|
|
Performance
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.7" data-path="faq.html">
|
|
|
|
<a href="faq.html">
|
|
|
|
|
|
FAQ
|
|
|
|
</a>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
<li class="chapter " data-level="1.8" >
|
|
|
|
<span>
|
|
|
|
|
|
TODO: Swagger & OpenAPI
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</li>
|
|
|
|
|
|
|
|
|
|
<li class="divider"></li>
|
|
|
|
<li>
|
|
<a href="https://www.gitbook.com" target="blank" class="gitbook-link">
|
|
Published with GitBook
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
|
|
|
<div class="book-body">
|
|
|
|
<div class="body-inner">
|
|
|
|
|
|
|
|
<div class="book-header" role="navigation">
|
|
|
|
|
|
<!-- Title -->
|
|
<h1>
|
|
<i class="fa fa-circle-o-notch fa-spin"></i>
|
|
<a href="." >Performance</a>
|
|
</h1>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="page-wrapper" tabindex="-1" role="main">
|
|
<div class="page-inner">
|
|
|
|
<div id="book-search-results">
|
|
<div class="search-noresults">
|
|
|
|
<section class="normal markdown-section">
|
|
|
|
<h1 id="performance">Performance</h1>
|
|
<p>There are many great routing libraries for Clojure(Script), but not many are optimized for perf. Reitit tries to be both great in features and be really fast. Originally the routing was adopted from <a href="http://pedestal.io/" target="_blank">Pedestal</a> (which is known to be fast), but has been partially rewritten performance in mind. Hopefully some optimizations can be back-ported to Pedestal.</p>
|
|
<h3 id="rationale">Rationale</h3>
|
|
<ul>
|
|
<li>Multiple routing algorithms, choose based on the route tree</li>
|
|
<li>Route flattening and re-ordering</li>
|
|
<li>Managed mutability over Immutability</li>
|
|
<li>Precompute/compile as much as possible (matches, middleware, routes)</li>
|
|
<li>Use abstractions that enable JVM optimizations</li>
|
|
<li>Use small functions to enable JVM Inlining</li>
|
|
<li>Protocols over Multimethods (or Maps)</li>
|
|
<li>Records over Maps</li>
|
|
<li>Always be measuring</li>
|
|
<li>Don't trust the (micro-)benchmarks</li>
|
|
</ul>
|
|
<h3 id="does-routing-performance-matter">Does routing performance matter?</h3>
|
|
<p>Well, it depends. With small route trees, it might not. But, with large (real-life) route trees, difference between the fastest and the slowest tested libs can be two or three orders of magnitude. For busy sites it actually matters if you routing request takes 100 ns or 100 µs. A lot.</p>
|
|
<h3 id="tests">Tests</h3>
|
|
<p>All perf tests are found in <a href="https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit" target="_blank">the repo</a> and have been run with the following setup:</p>
|
|
<pre><code>;;
|
|
;; 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
|
|
;;
|
|
</code></pre><p><strong>NOTE:</strong> Tests are not scientific proof and may contain errors. You should always run the perf tests with your own (real-life) routing tables to get more accurate results for your use case. Also, if you have idea how to test things better, please let us know.</p>
|
|
<h3 id="simple-example">Simple Example</h3>
|
|
<p>The routing sample taken from <a href="https://github.com/funcool/bide" target="_blank">bide</a> README:</p>
|
|
<pre><code class="lang-clj">(<span class="hljs-name">require</span> '[reitit.core <span class="hljs-symbol">:as</span> r])
|
|
(<span class="hljs-name">require</span> '[criterium.core <span class="hljs-symbol">:as</span> cc])
|
|
|
|
(<span class="hljs-name"><span class="hljs-builtin-name">def</span></span> routes
|
|
(<span class="hljs-name">r/router</span>
|
|
[[<span class="hljs-string">"/auth/login"</span> <span class="hljs-symbol">:auth/login</span>]
|
|
[<span class="hljs-string">"/auth/recovery/token/:token"</span> <span class="hljs-symbol">:auth/recovery</span>]
|
|
[<span class="hljs-string">"/workspace/:project/:page"</span> <span class="hljs-symbol">:workspace/page</span>]]))
|
|
|
|
<span class="hljs-comment">;; Execution time mean (per 1000) : 3.2 µs -> 312M ops/sec</span>
|
|
(<span class="hljs-name">cc/quick-bench</span>
|
|
(<span class="hljs-name"><span class="hljs-builtin-name">dotimes</span></span> [_ <span class="hljs-number">1000</span>]
|
|
(<span class="hljs-name">r/match-by-path</span> routes <span class="hljs-string">"/auth/login"</span>)))
|
|
|
|
<span class="hljs-comment">;; Execution time mean (per 1000): 530 µs -> 1.9M ops/sec</span>
|
|
(<span class="hljs-name">cc/quick-bench</span>
|
|
(<span class="hljs-name"><span class="hljs-builtin-name">dotimes</span></span> [_ <span class="hljs-number">1000</span>]
|
|
(<span class="hljs-name">r/match-by-path</span> routes <span class="hljs-string">"/workspace/1/1"</span>)))
|
|
</code></pre>
|
|
<p>Based on the <a href="https://github.com/metosin/reitit/tree/master/perf-test/clj/reitit/perf/bide_perf_test.clj" target="_blank">perf tests</a>, 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).</p>
|
|
<p>But, the example is too simple for any real benchmark. Also, some of the libraries always match on the <code>:request-method</code> too and by doing so, do more work than just match by path. Compojure does most work also by invoking the handler.</p>
|
|
<p>So, we need to test something more realistic.</p>
|
|
<h3 id="restful-apis">RESTful apis</h3>
|
|
<p>To get better view on the real life routing performance, there is <a href="https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/opensensors_perf_test.clj" target="_blank">test</a> 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 <a href="https://opensensors.io/" target="_blank">OpenSensors</a> swagger definitions.</p>
|
|
<p>Thanks to the snappy <a href="https://github.com/metosin/reitit/blob/master/modules/reitit-core/src/reitit/segment.cljc" target="_blank">segment-tree</a> algorithm, <code>reitit-ring</code> is fastest here. Pedestal is also fast with it's <a href="https://en.wikipedia.org/wiki/Radix_tree" target="_blank">prefix-tree</a> implementation.</p>
|
|
<p><img src="images/opensensors.png" alt="Opensensors perf test"></p>
|
|
<h3 id="cqrs-apis">CQRS apis</h3>
|
|
<p>Another real-life <a href="https://github.com/metosin/reitit/blob/master/perf-test/clj/reitit/lupapiste_perf_test.clj" target="_blank">test scenario</a> is a <a href="https://martinfowler.com/bliki/CQRS.html" target="_blank">CQRS</a>-style route tree, where all the paths are static, e.g. <code>/api/command/add-order</code>. The route definitions are pulled out from <a href="https://github.com/lupapiste/lupapiste" target="_blank">Lupapiste</a>. The test consists of ~300 static routes (just the commands here, there would be ~200 queries too).</p>
|
|
<p>Again, both <code>reitit-ring</code> and Pedestal shine here, thanks to the fast lookup-routers. On average, they are <strong>two</strong> and on best case, <strong>three orders of magnitude faster</strong> than the other tested libs. Ataraxy failed this test on <code>Method code too large!</code> error.</p>
|
|
<p><img src="images/lupapiste.png" alt="Opensensors perf test"></p>
|
|
<p><strong>NOTE</strong>: If there would be even one wildcard route in the route-tree, Pedestal would fallback from lookup-router to the prefix-tree router, yielding nearly constant, but an order of magnitude slower perf. Reitit instead fallbacks to <code>:mixed-router</code>, serving all the static routes with <code>:lookup-router</code>, just the wildcard route(s) with <code>:segment-tree</code>. So, the performance would not notably degrade.</p>
|
|
<h3 id="why-measure">Why measure?</h3>
|
|
<p>The routing perf needs to be measured to get an internal baseline to optimize against. We also want to ensure that new features don't regress the performance. Perf tests should be run in a stable CI environment. Help welcome.</p>
|
|
<h3 id="looking-out-of-the-box">Looking out of the box</h3>
|
|
<p>It might be interesting to compare reitit with the routers in other languages, like the <a href="https://github.com/julienschmidt/go-http-routing-benchmark" target="_blank">routers in Go</a>.</p>
|
|
<h3 id="performance-tips">Performance tips</h3>
|
|
<p>Few things that have an effect on performance:</p>
|
|
<ul>
|
|
<li>Wildcard-routes are an order of magnitude slower than static routes</li>
|
|
<li>It's ok to mix non-wildcard and wildcard routes in a same routing tree as long as you don't disable the <a href="basics/route_conflicts.html">conflict resolution</a> => if no conflicting routes are found, a <code>:mixed-router</code> can be created, which internally has a fast static path router and a separate wildcard-router. So, the static paths are still fast.</li>
|
|
<li>Move computation from request processing time into creation time, using by compiling <a href="ring/compiling_middleware.html">middleware</a> & <a href="advanced/configuring_routers.html">route data</a>.<ul>
|
|
<li>Unmounted middleware (or interceptor) is infinitely faster than a mounted one effectively doing nothing.</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
</section>
|
|
|
|
</div>
|
|
<div class="search-results">
|
|
<div class="has-results">
|
|
|
|
<h1 class="search-results-title"><span class='search-results-count'></span> results matching "<span class='search-query'></span>"</h1>
|
|
<ul class="search-results-list"></ul>
|
|
|
|
</div>
|
|
<div class="no-results">
|
|
|
|
<h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<a href="ring/compiling_middleware.html" class="navigation navigation-prev " aria-label="Previous page: Compiling Middleware">
|
|
<i class="fa fa-angle-left"></i>
|
|
</a>
|
|
|
|
|
|
<a href="faq.html" class="navigation navigation-next " aria-label="Next page: FAQ">
|
|
<i class="fa fa-angle-right"></i>
|
|
</a>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<script>
|
|
var gitbook = gitbook || [];
|
|
gitbook.push(function() {
|
|
gitbook.page.hasChanged({"page":{"title":"Performance","level":"1.6","depth":1,"next":{"title":"FAQ","level":"1.7","depth":1,"path":"faq.md","ref":"faq.md","articles":[]},"previous":{"title":"Compiling Middleware","level":"1.5.6","depth":2,"path":"ring/compiling_middleware.md","ref":"ring/compiling_middleware.md","articles":[]},"dir":"ltr"},"config":{"plugins":["editlink","github","highlight"],"root":"doc","styles":{"website":"styles/website.css","pdf":"styles/pdf.css","epub":"styles/epub.css","mobi":"styles/mobi.css","ebook":"styles/ebook.css","print":"styles/print.css"},"pluginsConfig":{"editlink":{"label":"Edit This Page","multilingual":false,"base":"https://github.com/metosin/reitit/tree/master/doc"},"github":{"url":"https://github.com/metosin/reitit"},"highlight":{},"search":{},"lunr":{"maxIndexSize":1000000,"ignoreSpecialCharacters":false},"sharing":{"facebook":true,"twitter":true,"google":false,"weibo":false,"instapaper":false,"vk":false,"all":["facebook","google","twitter","weibo","instapaper"]},"fontsettings":{"theme":"white","family":"sans","size":2},"theme-default":{"styles":{"website":"styles/website.css","pdf":"styles/pdf.css","epub":"styles/epub.css","mobi":"styles/mobi.css","ebook":"styles/ebook.css","print":"styles/print.css"},"showLevel":false}},"theme":"default","pdf":{"pageNumbers":true,"fontSize":12,"fontFamily":"Arial","paperSize":"a4","chapterMark":"pagebreak","pageBreaksBefore":"/","margin":{"right":62,"left":62,"top":56,"bottom":56}},"structure":{"langs":"LANGS.md","readme":"README.md","glossary":"GLOSSARY.md","summary":"SUMMARY.md"},"variables":{},"gitbook":"*"},"file":{"path":"performance.md","mtime":"2017-12-29T10:18:08.868Z","type":"markdown"},"gitbook":{"version":"3.2.3","time":"2017-12-29T10:18:58.002Z"},"basePath":".","book":{"language":""}});
|
|
});
|
|
</script>
|
|
</div>
|
|
|
|
|
|
<script src="gitbook/gitbook.js"></script>
|
|
<script src="gitbook/theme.js"></script>
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-editlink/plugin.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-github/plugin.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-search/search-engine.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-search/search.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-lunr/lunr.min.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-lunr/search-lunr.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-sharing/buttons.js"></script>
|
|
|
|
|
|
|
|
<script src="gitbook/gitbook-plugin-fontsettings/fontsettings.js"></script>
|
|
|
|
|
|
|
|
</body>
|
|
</html>
|
|
|