limits 4.5__tar.gz → 4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {limits-4.5 → limits-4.7}/HISTORY.rst +19 -0
- {limits-4.5 → limits-4.7}/PKG-INFO +1 -1
- {limits-4.5 → limits-4.7}/doc/source/conf.py +26 -1
- limits-4.7/doc/source/ext/_static/benchmark-chart.css +10 -0
- limits-4.7/doc/source/ext/_static/js/benchmark-chart.js +194 -0
- limits-4.7/doc/source/ext/_static/js/benchmark-details.js +87 -0
- limits-4.7/doc/source/ext/_static/js/benchmark-loader.js +32 -0
- limits-4.7/doc/source/ext/_templates/git_info.js +2 -0
- limits-4.7/doc/source/ext/bench_chart.py +128 -0
- {limits-4.5 → limits-4.7}/doc/source/index.rst +1 -0
- limits-4.7/doc/source/performance.rst +221 -0
- {limits-4.5 → limits-4.7}/doc/source/theme_config.py +18 -0
- {limits-4.5 → limits-4.7}/limits/_version.py +3 -3
- {limits-4.5 → limits-4.7}/limits/aio/storage/mongodb.py +3 -0
- {limits-4.5 → limits-4.7}/limits/storage/mongodb.py +4 -0
- {limits-4.5 → limits-4.7}/limits.egg-info/PKG-INFO +1 -1
- {limits-4.5 → limits-4.7}/limits.egg-info/SOURCES.txt +7 -0
- {limits-4.5 → limits-4.7}/CLASSIFIERS +0 -0
- {limits-4.5 → limits-4.7}/CONTRIBUTIONS.rst +0 -0
- {limits-4.5 → limits-4.7}/LICENSE.txt +0 -0
- {limits-4.5 → limits-4.7}/MANIFEST.in +0 -0
- {limits-4.5 → limits-4.7}/README.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/Makefile +0 -0
- {limits-4.5 → limits-4.7}/doc/source/_static/custom.css +0 -0
- {limits-4.5 → limits-4.7}/doc/source/api.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/async.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/changelog.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/custom-storage.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/installation.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/quickstart.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/storage.rst +0 -0
- {limits-4.5 → limits-4.7}/doc/source/strategies.rst +0 -0
- {limits-4.5 → limits-4.7}/limits/__init__.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/__init__.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/__init__.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/base.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/etcd.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/memcached.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/memory.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/redis/__init__.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/redis/bridge.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/redis/coredis.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/redis/redispy.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/storage/redis/valkey.py +0 -0
- {limits-4.5 → limits-4.7}/limits/aio/strategies.py +0 -0
- {limits-4.5 → limits-4.7}/limits/errors.py +0 -0
- {limits-4.5 → limits-4.7}/limits/limits.py +0 -0
- {limits-4.5 → limits-4.7}/limits/py.typed +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/__init__.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/base.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/etcd.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/memcached.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/memory.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/redis.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/redis_cluster.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/redis_sentinel.py +0 -0
- {limits-4.5 → limits-4.7}/limits/storage/registry.py +0 -0
- {limits-4.5 → limits-4.7}/limits/strategies.py +0 -0
- {limits-4.5 → limits-4.7}/limits/typing.py +0 -0
- {limits-4.5 → limits-4.7}/limits/util.py +0 -0
- {limits-4.5 → limits-4.7}/limits/version.py +0 -0
- {limits-4.5 → limits-4.7}/limits.egg-info/dependency_links.txt +0 -0
- {limits-4.5 → limits-4.7}/limits.egg-info/not-zip-safe +0 -0
- {limits-4.5 → limits-4.7}/limits.egg-info/requires.txt +0 -0
- {limits-4.5 → limits-4.7}/limits.egg-info/top_level.txt +0 -0
- {limits-4.5 → limits-4.7}/pyproject.toml +0 -0
- {limits-4.5 → limits-4.7}/requirements/ci.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/dev.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/docs.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/main.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/async-etcd.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/async-memcached.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/async-mongodb.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/async-redis.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/async-valkey.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/etcd.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/memcached.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/mongodb.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/redis.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/rediscluster.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/storage/valkey.txt +0 -0
- {limits-4.5 → limits-4.7}/requirements/test.txt +0 -0
- {limits-4.5 → limits-4.7}/setup.cfg +0 -0
- {limits-4.5 → limits-4.7}/setup.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_limit_granularities.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_limits.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_ratelimit_parser.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_storage.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_strategy.py +0 -0
- {limits-4.5 → limits-4.7}/tests/test_utils.py +0 -0
- {limits-4.5 → limits-4.7}/versioneer.py +0 -0
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
Changelog
|
|
4
4
|
=========
|
|
5
5
|
|
|
6
|
+
v4.7
|
|
7
|
+
----
|
|
8
|
+
Release Date: 2025-04-08
|
|
9
|
+
|
|
10
|
+
* Documentation
|
|
11
|
+
|
|
12
|
+
* Add benchmarking results in documentation
|
|
13
|
+
|
|
14
|
+
v4.6
|
|
15
|
+
----
|
|
16
|
+
Release Date: 2025-04-03
|
|
17
|
+
|
|
18
|
+
* Bug Fix
|
|
19
|
+
|
|
20
|
+
* Ensure mongo clients are closed on storage destruction.
|
|
21
|
+
`Issue 264 <https://github.com/alisaifee/limits/issues/264>`_
|
|
22
|
+
|
|
6
23
|
v4.5
|
|
7
24
|
----
|
|
8
25
|
Release Date: 2025-04-03
|
|
@@ -835,6 +852,8 @@ Release Date: 2015-01-08
|
|
|
835
852
|
|
|
836
853
|
|
|
837
854
|
|
|
855
|
+
|
|
856
|
+
|
|
838
857
|
|
|
839
858
|
|
|
840
859
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from docutils import nodes
|
|
8
|
+
from sphinx.application import Sphinx
|
|
9
|
+
from sphinx.util.docutils import SphinxDirective
|
|
5
10
|
|
|
6
11
|
sys.path.insert(0, os.path.abspath("../../"))
|
|
7
12
|
sys.path.insert(0, os.path.abspath("./"))
|
|
@@ -13,6 +18,7 @@ import limits
|
|
|
13
18
|
project = "limits"
|
|
14
19
|
description = "limits is a python library to perform rate limiting with commonly used storage backends"
|
|
15
20
|
copyright = "2023, Ali-Akber Saifee"
|
|
21
|
+
|
|
16
22
|
if ".post0.dev" in limits.__version__:
|
|
17
23
|
version, ahead = limits.__version__.split(".post0.dev")
|
|
18
24
|
else:
|
|
@@ -20,7 +26,22 @@ else:
|
|
|
20
26
|
|
|
21
27
|
release = version
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
if branch_from_env := os.environ.get("READTHEDOCS_VERSION", None):
|
|
31
|
+
benchmark_git_context = {
|
|
32
|
+
"branch": branch_from_env,
|
|
33
|
+
"sha": os.environ.get("READTHEDOCS_GIT_COMMIT_HASH", "")
|
|
34
|
+
}
|
|
35
|
+
else:
|
|
36
|
+
import limits._version
|
|
37
|
+
git_info = limits._version.git_pieces_from_vcs("", os.path.abspath("../../"), False)
|
|
38
|
+
benchmark_git_context = {
|
|
39
|
+
"branch": git_info.get("branch", ""),
|
|
40
|
+
"sha": git_info.get("long", None)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
html_static_path = ["_static"]
|
|
44
|
+
|
|
24
45
|
html_css_files = [
|
|
25
46
|
"custom.css",
|
|
26
47
|
"https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap",
|
|
@@ -38,6 +59,7 @@ try:
|
|
|
38
59
|
html_title = f"{project} <small><b style='color: var(--color-brand-primary)'>{{dev}}</b></small>"
|
|
39
60
|
except:
|
|
40
61
|
pass
|
|
62
|
+
sys.path.append(str(Path('ext').resolve()))
|
|
41
63
|
|
|
42
64
|
extensions = [
|
|
43
65
|
"sphinx.ext.autodoc",
|
|
@@ -53,6 +75,7 @@ extensions = [
|
|
|
53
75
|
"sphinx_copybutton",
|
|
54
76
|
"sphinx_inline_tabs",
|
|
55
77
|
"sphinx_paramlinks",
|
|
78
|
+
"bench_chart",
|
|
56
79
|
]
|
|
57
80
|
|
|
58
81
|
autodoc_default_options = {
|
|
@@ -70,6 +93,8 @@ autosectionlabel_prefix_document = True
|
|
|
70
93
|
|
|
71
94
|
extlinks = {"pypi": ("https://pypi.org/project/%s", "%s")}
|
|
72
95
|
|
|
96
|
+
copybutton_exclude = '.gp, .go'
|
|
97
|
+
|
|
73
98
|
intersphinx_mapping = {
|
|
74
99
|
"python": ("http://docs.python.org/", None),
|
|
75
100
|
"coredis": ("https://coredis.readthedocs.io/en/latest/", None),
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const KNOWN_PARAMS = ["storage_type", "limit", "strategy", "async"];
|
|
2
|
+
|
|
3
|
+
function getBenchmarkData(result, query) {
|
|
4
|
+
let benchmarks = result.benchmarks;
|
|
5
|
+
return benchmarks.filter(function (benchmark) {
|
|
6
|
+
let okay = true;
|
|
7
|
+
if (query) {
|
|
8
|
+
Object.entries(query).forEach((entry) => {
|
|
9
|
+
if (entry[0] != "group" && benchmark.params[entry[0]] != entry[1]) {
|
|
10
|
+
okay = false;
|
|
11
|
+
}
|
|
12
|
+
if (entry[0] == "group" && entry[1] != benchmark.group) {
|
|
13
|
+
okay = false;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return okay;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatRateLimit(str) {
|
|
22
|
+
var m = str.match(/(\d+(?:\.\d+)?)\s+per\s+1\s+(\w+)/i);
|
|
23
|
+
if (!m) return str;
|
|
24
|
+
var n = parseFloat(m[1]),
|
|
25
|
+
u = m[2].toLowerCase(),
|
|
26
|
+
num =
|
|
27
|
+
n >= 1000
|
|
28
|
+
? (n / 1000) % 1 === 0
|
|
29
|
+
? n / 1000 + "K"
|
|
30
|
+
: (n / 1000).toFixed(1) + "K"
|
|
31
|
+
: n.toString(),
|
|
32
|
+
umap = {
|
|
33
|
+
second: "s",
|
|
34
|
+
seconds: "s",
|
|
35
|
+
minute: "min",
|
|
36
|
+
minutes: "mins",
|
|
37
|
+
hour: "hr",
|
|
38
|
+
hours: "hr",
|
|
39
|
+
day: "day",
|
|
40
|
+
days: "day",
|
|
41
|
+
};
|
|
42
|
+
return num + "/" + (umap[u] || u);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nameTransform(benchmark, stripParams, query) {
|
|
46
|
+
let name = benchmark.name;
|
|
47
|
+
params = benchmark.params;
|
|
48
|
+
name = name
|
|
49
|
+
.replace(/\[.*?\]/, "")
|
|
50
|
+
.replace("_async", "")
|
|
51
|
+
.replaceAll("_", "-");
|
|
52
|
+
name = name.replace(benchmark.group, "");
|
|
53
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
54
|
+
let additional = getRemainingGroups(benchmark, query);
|
|
55
|
+
Object.entries(additional).forEach((param) => {
|
|
56
|
+
let value = param[1];
|
|
57
|
+
if (param[0] === "limit") {
|
|
58
|
+
value = formatRateLimit(value);
|
|
59
|
+
}
|
|
60
|
+
if (name) {
|
|
61
|
+
name += ` - ${value}`;
|
|
62
|
+
} else {
|
|
63
|
+
name = `${value}`;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getRemainingGroups(benchmark, query) {
|
|
70
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
71
|
+
let additional = {};
|
|
72
|
+
Object.entries(benchmark.params).forEach((param) => {
|
|
73
|
+
if (!queryParam.includes(param[0]) && KNOWN_PARAMS.includes(param[0])) {
|
|
74
|
+
additional[param[0]] = param[1];
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return additional;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getColorForStorage(storageType) {
|
|
81
|
+
const storageColorMap = {
|
|
82
|
+
memory: window
|
|
83
|
+
.getComputedStyle(document.body)
|
|
84
|
+
.getPropertyValue("--color-purple"),
|
|
85
|
+
mongodb: window
|
|
86
|
+
.getComputedStyle(document.body)
|
|
87
|
+
.getPropertyValue("--color-yellow"),
|
|
88
|
+
memcached: window
|
|
89
|
+
.getComputedStyle(document.body)
|
|
90
|
+
.getPropertyValue("--color-aqua"),
|
|
91
|
+
redis: window
|
|
92
|
+
.getComputedStyle(document.body)
|
|
93
|
+
.getPropertyValue("--color-red"),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Fallback color if an unknown storageType appears
|
|
97
|
+
return storageColorMap[storageType] || "#7f7f7f"; // gray
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sortBenchmarksByParams(benchmarks, sortKeys) {
|
|
101
|
+
return benchmarks.sort((a, b) => {
|
|
102
|
+
for (const key of sortKeys) {
|
|
103
|
+
let valA = (a.params?.[key] || "").toLowerCase();
|
|
104
|
+
let valB = (b.params?.[key] || "").toLowerCase();
|
|
105
|
+
if (key == "limit") {
|
|
106
|
+
valA = parseInt(valA.split(" ")[0]);
|
|
107
|
+
valB = parseInt(valB.split(" ")[0]);
|
|
108
|
+
}
|
|
109
|
+
return valA < valB ? -1 : valA > valB ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return a.name.localeCompare(b.name);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function sortBenchmarksByParams(benchmarks, sortKeys) {
|
|
116
|
+
return benchmarks.sort(function (a, b) {
|
|
117
|
+
for (const key of sortKeys) {
|
|
118
|
+
let valA = (a.params?.[key] || "").toLowerCase();
|
|
119
|
+
let valB = (b.params?.[key] || "").toLowerCase();
|
|
120
|
+
if (key === "limit") {
|
|
121
|
+
valA = parseInt(valA.split(" ")[0], 10);
|
|
122
|
+
valB = parseInt(valB.split(" ")[0], 10);
|
|
123
|
+
}
|
|
124
|
+
if (valA < valB) return -1;
|
|
125
|
+
if (valA > valB) return 1;
|
|
126
|
+
}
|
|
127
|
+
return a.name.localeCompare(b.name);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
let dispatched = new Set();
|
|
131
|
+
|
|
132
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
133
|
+
const charts = document.querySelectorAll(".benchmark-chart");
|
|
134
|
+
charts.forEach((chart) => {
|
|
135
|
+
let source = chart.dataset.source;
|
|
136
|
+
let filters = JSON.parse(chart.dataset.filters);
|
|
137
|
+
let query = JSON.parse(chart.dataset.query);
|
|
138
|
+
let sortBy = JSON.parse(
|
|
139
|
+
chart.dataset.sortBy || '["storage_type", "limit"]',
|
|
140
|
+
);
|
|
141
|
+
if (!dispatched.has(source)) {
|
|
142
|
+
fetchBenchmarkData(`${source}.json`).then((result) => {
|
|
143
|
+
window.Benchmarks[source] = result;
|
|
144
|
+
event = new Event(`${source}-loaded`);
|
|
145
|
+
window.dispatchEvent(event);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
dispatched.add(source);
|
|
149
|
+
window.addEventListener(`${chart.dataset.source}-loaded`, function () {
|
|
150
|
+
let results = Benchmarks[chart.dataset.source];
|
|
151
|
+
let unsorted = getBenchmarkData(results, query);
|
|
152
|
+
let data = sortBenchmarksByParams(
|
|
153
|
+
getBenchmarkData(results, query),
|
|
154
|
+
sortBy,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const layout = {
|
|
158
|
+
yaxis: {
|
|
159
|
+
title: { text: "Time (ms)" },
|
|
160
|
+
exponentformat: "none",
|
|
161
|
+
ticksuffix: " ms",
|
|
162
|
+
tickformat: ",.2f",
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
Plotly.newPlot(
|
|
166
|
+
chart,
|
|
167
|
+
data.map(function (benchmark) {
|
|
168
|
+
let item = {
|
|
169
|
+
type: "box",
|
|
170
|
+
name: nameTransform(benchmark, true, query),
|
|
171
|
+
y: benchmark.stats.data || [
|
|
172
|
+
benchmark.stats.min * 1e3,
|
|
173
|
+
benchmark.stats.q1 * 1e3,
|
|
174
|
+
benchmark.stats.median * 1e3,
|
|
175
|
+
benchmark.stats.q3 * 1e3,
|
|
176
|
+
benchmark.stats.max * 1e3,
|
|
177
|
+
],
|
|
178
|
+
boxmean: true,
|
|
179
|
+
boxpoints: false,
|
|
180
|
+
line: { width: 1 },
|
|
181
|
+
marker: {
|
|
182
|
+
color: getColorForStorage(benchmark.params.storage_type),
|
|
183
|
+
},
|
|
184
|
+
showlegend: true,
|
|
185
|
+
legendgroup: benchmark.params.storage_type,
|
|
186
|
+
legendgrouptitle: { text: benchmark.params.storage_type },
|
|
187
|
+
};
|
|
188
|
+
return item;
|
|
189
|
+
}),
|
|
190
|
+
layout,
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { render, html } from "https://unpkg.com/uhtml@3.2.1?module";
|
|
2
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
3
|
+
const details = document.querySelectorAll(".benchmark-details");
|
|
4
|
+
details.forEach((detail) => {
|
|
5
|
+
let source = detail.dataset.source;
|
|
6
|
+
if (!dispatched.has(source)) {
|
|
7
|
+
fetchBenchmarkData(`${source}.json`).then((result) => {
|
|
8
|
+
window.Benchmarks[source] = result;
|
|
9
|
+
event = new Event(`${source}-loaded`);
|
|
10
|
+
window.dispatchEvent(event);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
dispatched.add(source);
|
|
14
|
+
window.addEventListener(`${detail.dataset.source}-loaded`, function () {
|
|
15
|
+
const machine_info = window.Benchmarks[source].machine_info;
|
|
16
|
+
const commit_info = window.Benchmarks[source].commit_info;
|
|
17
|
+
const cpu = window.Benchmarks[source].machine_info.cpu;
|
|
18
|
+
console.log(machine_info);
|
|
19
|
+
render(
|
|
20
|
+
detail,
|
|
21
|
+
html`
|
|
22
|
+
<table class="benchmark-details-section">
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>Machine Information</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<tr>
|
|
30
|
+
<td>Operating System</td>
|
|
31
|
+
<td>${machine_info.system} (${machine_info.release})</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<tr>
|
|
34
|
+
<td>CPU</td>
|
|
35
|
+
<td>
|
|
36
|
+
${cpu.brand_raw} ${cpu.processor} @ ${cpu.hz_actual_friendly}
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td>Python</td>
|
|
41
|
+
<td>${machine_info.python_version}</td>
|
|
42
|
+
</tr>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
<table class="benchmark-details-section">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Source Information</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
<tr>
|
|
53
|
+
<td>Branch</td>
|
|
54
|
+
<td>${commit_info.branch}</td>
|
|
55
|
+
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<td>Commit Hash</td>
|
|
58
|
+
<td>${commit_info.id}</td>
|
|
59
|
+
</tr>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
<table class="benchmark-details-section">
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th>Storage Information</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<tr>
|
|
70
|
+
<td>Redis</td>
|
|
71
|
+
<td>${machine_info.redis.redis_version}</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>Memcached</td>
|
|
75
|
+
<td>${machine_info.memcached.version}</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<tr>
|
|
78
|
+
<td>MongoDB</td>
|
|
79
|
+
<td>${machine_info.mongodb.version}</td>
|
|
80
|
+
</tr>
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
`,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const BENCHMARK_PATHS = [
|
|
2
|
+
`https://${GITSHA}--py-limits.netlify.app/`,
|
|
3
|
+
`https://${GITBRANCH}--py-limits.netlify.app/`,
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
window.Benchmarks = new Map();
|
|
7
|
+
|
|
8
|
+
function fetchBenchmarkData(filename) {
|
|
9
|
+
let attempts = 0;
|
|
10
|
+
|
|
11
|
+
function tryFetch() {
|
|
12
|
+
if (attempts >= BENCHMARK_PATHS.length) {
|
|
13
|
+
return Promise.reject("All fetch attempts failed.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const base = BENCHMARK_PATHS[attempts++];
|
|
17
|
+
const url = base + filename;
|
|
18
|
+
|
|
19
|
+
// First send a HEAD request to quietly check existence
|
|
20
|
+
return fetch(url, { method: "HEAD" })
|
|
21
|
+
.then((headRes) => {
|
|
22
|
+
if (!headRes.ok) throw new Error("HEAD check failed");
|
|
23
|
+
return fetch(url).then((res) => {
|
|
24
|
+
if (!res.ok) throw new Error(`GET failed from ${url}`);
|
|
25
|
+
return res.json();
|
|
26
|
+
});
|
|
27
|
+
})
|
|
28
|
+
.catch(() => tryFetch());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return tryFetch();
|
|
32
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from docutils import nodes
|
|
8
|
+
from sphinx.util.docutils import SphinxDirective
|
|
9
|
+
from sphinx.util.osutil import ensuredir
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sphinx.application import Sphinx
|
|
13
|
+
|
|
14
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
|
|
16
|
+
def check_bool(value):
|
|
17
|
+
if value.lower() in ["true", "false"]:
|
|
18
|
+
return value.lower() == "true"
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
def query(argument):
|
|
22
|
+
if not argument.strip():
|
|
23
|
+
return {}
|
|
24
|
+
queries = {}
|
|
25
|
+
for query in argument.strip().split(","):
|
|
26
|
+
key, value = query.split("=")
|
|
27
|
+
queries[key] = check_bool(value)
|
|
28
|
+
return queries
|
|
29
|
+
|
|
30
|
+
def filters(argument):
|
|
31
|
+
filters: dict[str, list|bool] = {}
|
|
32
|
+
for filter in argument.strip().split(","):
|
|
33
|
+
if ":" in filter:
|
|
34
|
+
source, value = filter.split(":")
|
|
35
|
+
filters.setdefault(source, []).append(value)
|
|
36
|
+
else:
|
|
37
|
+
filters[filter] = True
|
|
38
|
+
return filters
|
|
39
|
+
|
|
40
|
+
def sortBy(argument):
|
|
41
|
+
return [k.strip() for k in argument.split(",")] if argument else []
|
|
42
|
+
|
|
43
|
+
class BenchmarkDetails(SphinxDirective):
|
|
44
|
+
required_arguments = 0
|
|
45
|
+
final_argument_whitespace = False
|
|
46
|
+
option_spec = {
|
|
47
|
+
"source": str,
|
|
48
|
+
}
|
|
49
|
+
has_content = False
|
|
50
|
+
def run(self):
|
|
51
|
+
source = self.options.get("source", "benchmark-summary")
|
|
52
|
+
html = f"""
|
|
53
|
+
<div
|
|
54
|
+
class='benchmark-details'
|
|
55
|
+
data-source='{source}'
|
|
56
|
+
</div>
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
return [nodes.raw("", html, format="html")]
|
|
60
|
+
|
|
61
|
+
class BenchmarkChart(SphinxDirective):
|
|
62
|
+
required_arguments = 0
|
|
63
|
+
final_argument_whitespace = False
|
|
64
|
+
option_spec = {
|
|
65
|
+
"source": str,
|
|
66
|
+
"query": query,
|
|
67
|
+
"filters": filters,
|
|
68
|
+
"sort": sortBy,
|
|
69
|
+
}
|
|
70
|
+
has_content = False
|
|
71
|
+
def run(self):
|
|
72
|
+
source = self.options.get("source", "benchmark-summary")
|
|
73
|
+
filters = self.options.get("filters", ["group"])
|
|
74
|
+
query = self.options.get("query", {})
|
|
75
|
+
sortBy = self.options.get("sort", [])
|
|
76
|
+
|
|
77
|
+
html = f"""
|
|
78
|
+
<div
|
|
79
|
+
class='benchmark-chart'
|
|
80
|
+
data-source='{source}'
|
|
81
|
+
data-filters='{json.dumps(filters)}'
|
|
82
|
+
data-query='{json.dumps(query)}'
|
|
83
|
+
data-sortBy='{json.dumps(sortBy)}'>
|
|
84
|
+
</div>
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return [nodes.raw("", html, format="html")]
|
|
88
|
+
|
|
89
|
+
def render_js_template(app) -> None:
|
|
90
|
+
context = {
|
|
91
|
+
"branch": app.config.benchmark_git_context.get("branch", ""),
|
|
92
|
+
"sha": app.config.benchmark_git_context.get("sha", ""),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
template = app.builder.templates.environment.get_template("git_info.js")
|
|
96
|
+
rendered_js = template.render(context)
|
|
97
|
+
|
|
98
|
+
out_dir = os.path.join(app.outdir, "_static", "js")
|
|
99
|
+
ensuredir(out_dir)
|
|
100
|
+
out_path = os.path.join(out_dir, "git_info.js")
|
|
101
|
+
|
|
102
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
103
|
+
f.write(rendered_js)
|
|
104
|
+
app.add_js_file("js/git_info.js")
|
|
105
|
+
|
|
106
|
+
def setup(app: Sphinx):
|
|
107
|
+
app.add_directive("benchmark-chart", BenchmarkChart)
|
|
108
|
+
app.add_directive("benchmark-details", BenchmarkDetails)
|
|
109
|
+
app.add_config_value("benchmark_git_context", default={}, rebuild="env")
|
|
110
|
+
def add_assets(app, env) -> None:
|
|
111
|
+
static_path = os.path.join(here, "_static")
|
|
112
|
+
if static_path not in app.config.html_static_path:
|
|
113
|
+
app.config.html_static_path.append(static_path)
|
|
114
|
+
app.add_js_file("js/benchmark-chart.js")
|
|
115
|
+
app.add_js_file("js/benchmark-details.js", type="module")
|
|
116
|
+
app.add_js_file("js/benchmark-loader.js")
|
|
117
|
+
app.add_js_file("https://cdn.plot.ly/plotly-3.0.1.min.js")
|
|
118
|
+
app.add_css_file("benchmark-chart.css")
|
|
119
|
+
app.config.templates_path += [os.path.join(here, "_templates")]
|
|
120
|
+
app.connect("env-updated", add_assets)
|
|
121
|
+
app.connect("builder-inited", render_js_template)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"version": "0.1",
|
|
125
|
+
"parallel_read_safe": True,
|
|
126
|
+
"parallel_write_safe": True,
|
|
127
|
+
|
|
128
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Performance
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
The performance of each rate-limiting strategy and storage backend
|
|
5
|
+
differs in both throughput and storage cost characteristics.
|
|
6
|
+
|
|
7
|
+
Performance by storage and strategy
|
|
8
|
+
-----------------------------------
|
|
9
|
+
Below you will find benchmarks for each strategy and storage when using
|
|
10
|
+
a rate limit of ``500/minute``. (For details about the benchmarking environment
|
|
11
|
+
please refer to :ref:`performance:benchmark run details`).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
.. tab:: Hit
|
|
15
|
+
|
|
16
|
+
.. benchmark-chart::
|
|
17
|
+
:source: benchmark-summary
|
|
18
|
+
:query: limit=500 per 1 minute,group=hit,async=false
|
|
19
|
+
:sort: storage_type,strategy
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
.. tab:: Hit (Async)
|
|
23
|
+
|
|
24
|
+
.. benchmark-chart::
|
|
25
|
+
:source: benchmark-summary
|
|
26
|
+
:query: limit=500 per 1 minute,group=hit,async=true
|
|
27
|
+
:sort: storage_type,strategy
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
.. tab:: Test
|
|
32
|
+
|
|
33
|
+
.. benchmark-chart::
|
|
34
|
+
:source: benchmark-summary
|
|
35
|
+
:query: limit=500 per 1 minute,group=test,async=false
|
|
36
|
+
:sort: storage_type,strategy
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
.. tab:: Test (Async)
|
|
40
|
+
|
|
41
|
+
.. benchmark-chart::
|
|
42
|
+
:source: benchmark-summary
|
|
43
|
+
:query: limit=500 per 1 minute,group=test,async=true
|
|
44
|
+
:sort: storage_type,strategy
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
.. tab:: Get Window Stats
|
|
49
|
+
|
|
50
|
+
.. benchmark-chart::
|
|
51
|
+
:source: benchmark-summary
|
|
52
|
+
:query: limit=500 per 1 minute,group=get-window-stats,async=false
|
|
53
|
+
:sort: storage_type,strategy
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
.. tab:: Get Window Stats (Async)
|
|
57
|
+
|
|
58
|
+
.. benchmark-chart::
|
|
59
|
+
:source: benchmark-summary
|
|
60
|
+
:query: limit=500 per 1 minute,group=get-window-stats,async=true
|
|
61
|
+
:sort: storage_type,strategy
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Performance implication of limit sizes
|
|
65
|
+
--------------------------------------
|
|
66
|
+
|
|
67
|
+
Though for :ref:`strategies:fixed window` and :ref:`strategies:sliding window counter` both the
|
|
68
|
+
storage cost and performance of operations remains constant when the limit window and size varies,
|
|
69
|
+
this is not true for :ref:`strategies:moving window` which maintains a complete log of successful
|
|
70
|
+
requests within the window.
|
|
71
|
+
|
|
72
|
+
The following benchmarks demonstrate the implications when using various limits.
|
|
73
|
+
|
|
74
|
+
Fixed Window
|
|
75
|
+
~~~~~~~~~~~~
|
|
76
|
+
|
|
77
|
+
.. tab:: Hit
|
|
78
|
+
|
|
79
|
+
.. benchmark-chart::
|
|
80
|
+
:source: benchmark-summary
|
|
81
|
+
:query: async=false,group=hit,strategy=fixed-window
|
|
82
|
+
:sort: storage_type,limit
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
.. tab:: Hit (Async)
|
|
86
|
+
|
|
87
|
+
.. benchmark-chart::
|
|
88
|
+
:source: benchmark-summary
|
|
89
|
+
:query: async=true,group=hit,strategy=fixed-window
|
|
90
|
+
:sort: storage_type,limit
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
.. tab:: Test
|
|
94
|
+
|
|
95
|
+
.. benchmark-chart::
|
|
96
|
+
:source: benchmark-summary
|
|
97
|
+
:query: async=false,group=test,strategy=fixed-window
|
|
98
|
+
:sort: storage_type,limit
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
.. tab:: Test (Async)
|
|
102
|
+
|
|
103
|
+
.. benchmark-chart::
|
|
104
|
+
:source: benchmark-summary
|
|
105
|
+
:query: async=true,group=test,strategy=fixed-window
|
|
106
|
+
:sort: storage_type,limit
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
.. tab:: Get Window Stats
|
|
110
|
+
|
|
111
|
+
.. benchmark-chart::
|
|
112
|
+
:source: benchmark-summary
|
|
113
|
+
:query: async=false,group=get-window-stats,strategy=fixed-window
|
|
114
|
+
:sort: storage_type,limit
|
|
115
|
+
|
|
116
|
+
.. tab:: Get Window Stats (Async)
|
|
117
|
+
|
|
118
|
+
.. benchmark-chart::
|
|
119
|
+
:source: benchmark-summary
|
|
120
|
+
:query: async=true,group=get-window-stats,strategy=fixed-window
|
|
121
|
+
:sort: storage_type,limit
|
|
122
|
+
|
|
123
|
+
Moving Window
|
|
124
|
+
~~~~~~~~~~~~~
|
|
125
|
+
|
|
126
|
+
.. tab:: Hit
|
|
127
|
+
|
|
128
|
+
.. benchmark-chart::
|
|
129
|
+
:source: benchmark-summary
|
|
130
|
+
:query: async=false,group=hit,strategy=moving-window
|
|
131
|
+
:sort: storage_type,limit
|
|
132
|
+
|
|
133
|
+
.. tab:: Hit (Async)
|
|
134
|
+
|
|
135
|
+
.. benchmark-chart::
|
|
136
|
+
:source: benchmark-summary
|
|
137
|
+
:query: async=true,group=hit,strategy=moving-window
|
|
138
|
+
:sort: storage_type,limit
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
.. tab:: Test
|
|
142
|
+
|
|
143
|
+
.. benchmark-chart::
|
|
144
|
+
:source: benchmark-summary
|
|
145
|
+
:query: async=false,group=test,strategy=moving-window
|
|
146
|
+
:sort: storage_type,limit
|
|
147
|
+
|
|
148
|
+
.. tab:: Test (Async)
|
|
149
|
+
|
|
150
|
+
.. benchmark-chart::
|
|
151
|
+
:source: benchmark-summary
|
|
152
|
+
:query: async=true,group=test,strategy=moving-window
|
|
153
|
+
:sort: storage_type,limit
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
.. tab:: Get Window Stats
|
|
157
|
+
|
|
158
|
+
.. benchmark-chart::
|
|
159
|
+
:source: benchmark-summary
|
|
160
|
+
:query: async=false,group=get-window-stats,strategy=moving-window
|
|
161
|
+
:sort: storage_type,limit
|
|
162
|
+
|
|
163
|
+
.. tab:: Get Window Stats (Async)
|
|
164
|
+
|
|
165
|
+
.. benchmark-chart::
|
|
166
|
+
:source: benchmark-summary
|
|
167
|
+
:query: async=true,group=get-window-stats,strategy=moving-window
|
|
168
|
+
:sort: storage_type,limit
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
Sliding Window
|
|
172
|
+
~~~~~~~~~~~~~~
|
|
173
|
+
|
|
174
|
+
.. tab:: Hit
|
|
175
|
+
|
|
176
|
+
.. benchmark-chart::
|
|
177
|
+
:source: benchmark-summary
|
|
178
|
+
:query: async=false,group=hit,strategy=sliding-window
|
|
179
|
+
:sort: storage_type,limit
|
|
180
|
+
|
|
181
|
+
.. tab:: Hit (Async)
|
|
182
|
+
|
|
183
|
+
.. benchmark-chart::
|
|
184
|
+
:source: benchmark-summary
|
|
185
|
+
:query: async=true,group=hit,strategy=sliding-window
|
|
186
|
+
:sort: storage_type,limit
|
|
187
|
+
|
|
188
|
+
.. tab:: Test
|
|
189
|
+
|
|
190
|
+
.. benchmark-chart::
|
|
191
|
+
:source: benchmark-summary
|
|
192
|
+
:query: async=false,group=test,strategy=sliding-window
|
|
193
|
+
:sort: storage_type,limit
|
|
194
|
+
|
|
195
|
+
.. tab:: Test (Async)
|
|
196
|
+
|
|
197
|
+
.. benchmark-chart::
|
|
198
|
+
:source: benchmark-summary
|
|
199
|
+
:query: async=true,group=test,strategy=sliding-window
|
|
200
|
+
:sort: storage_type,limit
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
.. tab:: Get Window Stats
|
|
204
|
+
|
|
205
|
+
.. benchmark-chart::
|
|
206
|
+
:source: benchmark-summary
|
|
207
|
+
:query: async=false,group=get-window-stats,strategy=sliding-window
|
|
208
|
+
:sort: storage_type,limit
|
|
209
|
+
|
|
210
|
+
.. tab:: Get Window (Async)
|
|
211
|
+
|
|
212
|
+
.. benchmark-chart::
|
|
213
|
+
:source: benchmark-summary
|
|
214
|
+
:query: async=true,group=get-window-stats,strategy=sliding-window
|
|
215
|
+
:sort: storage_type,limit
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
Benchmark run details
|
|
219
|
+
---------------------
|
|
220
|
+
.. benchmark-details::
|
|
221
|
+
:source: benchmark-summary
|
|
@@ -33,6 +33,15 @@ html_theme_options = {
|
|
|
33
33
|
"font-stack--monospace": "Fira Code, monospace",
|
|
34
34
|
"color-brand-primary": colors["purple2"],
|
|
35
35
|
"color-brand-content": colors["blue2"],
|
|
36
|
+
"color-bg": colors["bg2"],
|
|
37
|
+
"color-fg": colors["fg2"],
|
|
38
|
+
"color-red": colors["red2"],
|
|
39
|
+
"color-orange": colors["orange2"],
|
|
40
|
+
"color-yellow": colors["yellow2"],
|
|
41
|
+
"color-green": colors["green2"],
|
|
42
|
+
"color-aqua": colors["aqua2"],
|
|
43
|
+
"color-blue": colors["blue2"],
|
|
44
|
+
"color-purple": colors["purple2"],
|
|
36
45
|
},
|
|
37
46
|
"dark_css_variables": {
|
|
38
47
|
"color-brand-primary": colors["purple"],
|
|
@@ -43,6 +52,15 @@ html_theme_options = {
|
|
|
43
52
|
"color-foreground-secondary": colors["bg1"],
|
|
44
53
|
"color-highlighted-background": colors["yellow"],
|
|
45
54
|
"color-highlight-on-target": colors["fg2"],
|
|
55
|
+
"color-bg": colors["fg1"],
|
|
56
|
+
"color-fg": colors["bg0"],
|
|
57
|
+
"color-red": colors["red"],
|
|
58
|
+
"color-orange": colors["orange"],
|
|
59
|
+
"color-yellow": colors["yellow"],
|
|
60
|
+
"color-green": colors["green"],
|
|
61
|
+
"color-aqua": colors["aqua"],
|
|
62
|
+
"color-blue": colors["blue"],
|
|
63
|
+
"color-purple": colors["purple"],
|
|
46
64
|
},
|
|
47
65
|
}
|
|
48
66
|
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-04-
|
|
11
|
+
"date": "2025-04-08T13:37:22-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "4.
|
|
14
|
+
"full-revisionid": "c20794b223ba2e7984ecf12c4395c092aadcad88",
|
|
15
|
+
"version": "4.7"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -16,11 +16,18 @@ doc/source/conf.py
|
|
|
16
16
|
doc/source/custom-storage.rst
|
|
17
17
|
doc/source/index.rst
|
|
18
18
|
doc/source/installation.rst
|
|
19
|
+
doc/source/performance.rst
|
|
19
20
|
doc/source/quickstart.rst
|
|
20
21
|
doc/source/storage.rst
|
|
21
22
|
doc/source/strategies.rst
|
|
22
23
|
doc/source/theme_config.py
|
|
23
24
|
doc/source/_static/custom.css
|
|
25
|
+
doc/source/ext/bench_chart.py
|
|
26
|
+
doc/source/ext/_static/benchmark-chart.css
|
|
27
|
+
doc/source/ext/_static/js/benchmark-chart.js
|
|
28
|
+
doc/source/ext/_static/js/benchmark-details.js
|
|
29
|
+
doc/source/ext/_static/js/benchmark-loader.js
|
|
30
|
+
doc/source/ext/_templates/git_info.js
|
|
24
31
|
limits/__init__.py
|
|
25
32
|
limits/_version.py
|
|
26
33
|
limits/errors.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|