limits 4.7.2__tar.gz → 4.7.3__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.7.2 → limits-4.7.3}/HISTORY.rst +13 -0
- {limits-4.7.2 → limits-4.7.3}/PKG-INFO +1 -1
- {limits-4.7.2 → limits-4.7.3}/doc/source/conf.py +10 -1
- {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/benchmark-chart.css +48 -12
- limits-4.7.3/doc/source/ext/_static/js/benchmark-chart.js +343 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/ext/bench_chart.py +12 -9
- limits-4.7.3/doc/source/performance.rst +126 -0
- {limits-4.7.2 → limits-4.7.3}/limits/_version.py +3 -3
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/memcached.py +4 -1
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/PKG-INFO +1 -1
- {limits-4.7.2 → limits-4.7.3}/requirements/docs.txt +3 -3
- limits-4.7.2/doc/source/ext/_static/js/benchmark-chart.js +0 -219
- limits-4.7.2/doc/source/performance.rst +0 -221
- {limits-4.7.2 → limits-4.7.3}/CLASSIFIERS +0 -0
- {limits-4.7.2 → limits-4.7.3}/CONTRIBUTIONS.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/LICENSE.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/MANIFEST.in +0 -0
- {limits-4.7.2 → limits-4.7.3}/README.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/Makefile +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/_static/custom.css +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/api.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/async.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/changelog.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/custom-storage.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/js/benchmark-details.js +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/js/benchmark-loader.js +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_templates/git_info.js +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/index.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/installation.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/quickstart.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/storage.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/strategies.rst +0 -0
- {limits-4.7.2 → limits-4.7.3}/doc/source/theme_config.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/__init__.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/__init__.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/__init__.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/base.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/etcd.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/memory.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/mongodb.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/__init__.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/bridge.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/coredis.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/redispy.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/valkey.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/aio/strategies.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/errors.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/limits.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/py.typed +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/__init__.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/base.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/etcd.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/memcached.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/memory.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/mongodb.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/redis.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/redis_cluster.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/redis_sentinel.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/storage/registry.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/strategies.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/typing.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/util.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits/version.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/SOURCES.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/dependency_links.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/not-zip-safe +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/requires.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/limits.egg-info/top_level.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/pyproject.toml +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/ci.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/dev.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/main.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-etcd.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-memcached.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-mongodb.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-redis.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-valkey.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/etcd.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/memcached.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/mongodb.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/redis.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/rediscluster.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/storage/valkey.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/requirements/test.txt +0 -0
- {limits-4.7.2 → limits-4.7.3}/setup.cfg +0 -0
- {limits-4.7.2 → limits-4.7.3}/setup.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_limit_granularities.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_limits.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_ratelimit_parser.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_storage.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_strategy.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/tests/test_utils.py +0 -0
- {limits-4.7.2 → limits-4.7.3}/versioneer.py +0 -0
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
Changelog
|
|
4
4
|
=========
|
|
5
5
|
|
|
6
|
+
v4.7.3
|
|
7
|
+
------
|
|
8
|
+
Release Date: 2025-04-12
|
|
9
|
+
|
|
10
|
+
* Documentation
|
|
11
|
+
|
|
12
|
+
* Expand benchmark results to included preseeded limits
|
|
13
|
+
|
|
14
|
+
* Bug Fix
|
|
15
|
+
|
|
16
|
+
* Handle clearing missing key with memcache + async
|
|
17
|
+
|
|
6
18
|
v4.7.2
|
|
7
19
|
------
|
|
8
20
|
Release Date: 2025-04-09
|
|
@@ -873,5 +885,6 @@ Release Date: 2015-01-08
|
|
|
873
885
|
|
|
874
886
|
|
|
875
887
|
|
|
888
|
+
|
|
876
889
|
|
|
877
890
|
|
|
@@ -39,7 +39,15 @@ else:
|
|
|
39
39
|
"branch": git_info.get("branch", ""),
|
|
40
40
|
"sha": git_info.get("long", None)
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
benchmark_param_mapping = {
|
|
43
|
+
"percentage_full": {
|
|
44
|
+
"display": "Percentage Seeded",
|
|
45
|
+
"info": "Percentage of rate limit already filled before the benchmark is run",
|
|
46
|
+
},
|
|
47
|
+
"async": {"display": "Asyncio", "info": "Using asyncio storage implementation"},
|
|
48
|
+
"storage_type": {"display": "Storage", "info": "Storage Backend"},
|
|
49
|
+
"strategy": {"display": "Strategy", "info": "Rate Limiting Strategy"},
|
|
50
|
+
}
|
|
43
51
|
html_static_path = ["_static"]
|
|
44
52
|
|
|
45
53
|
html_css_files = [
|
|
@@ -73,6 +81,7 @@ extensions = [
|
|
|
73
81
|
"sphinxext.opengraph",
|
|
74
82
|
"sphinxcontrib.programoutput",
|
|
75
83
|
"sphinx_copybutton",
|
|
84
|
+
"sphinx_design",
|
|
76
85
|
"sphinx_inline_tabs",
|
|
77
86
|
"sphinx_paramlinks",
|
|
78
87
|
"bench_chart",
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
.plot-container {
|
|
2
|
+
filter: invert(100%);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body[data-theme="light"] .plot-container {
|
|
6
|
+
filter: invert(0);
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
.benchmark-chart {
|
|
2
10
|
width: 100%;
|
|
3
11
|
}
|
|
@@ -34,28 +42,56 @@
|
|
|
34
42
|
0% {
|
|
35
43
|
opacity: 0;
|
|
36
44
|
}
|
|
37
|
-
,
|
|
38
45
|
25% {
|
|
39
|
-
opacity: 25
|
|
46
|
+
opacity: 0.25;
|
|
40
47
|
}
|
|
41
|
-
,
|
|
42
48
|
50% {
|
|
43
|
-
opacity:
|
|
49
|
+
opacity: 0.5;
|
|
44
50
|
}
|
|
45
|
-
,
|
|
46
51
|
90% {
|
|
47
|
-
opacity:
|
|
52
|
+
opacity: 0.9;
|
|
48
53
|
}
|
|
49
|
-
,
|
|
50
54
|
100% {
|
|
51
|
-
opacity:
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
.benchmark-filters {
|
|
59
|
+
padding-top: 1em;
|
|
60
|
+
padding-bottom: 1em;
|
|
61
|
+
display: flex;
|
|
62
|
+
justify-content: flex-end;
|
|
63
|
+
}
|
|
64
|
+
.benchmark-filter-dropdowns {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: flex-end;
|
|
67
|
+
justify-content: flex-end;
|
|
68
|
+
margin-top: 2em;
|
|
69
|
+
flex-direction: row;
|
|
70
|
+
@media only screen and (max-width: 800px) {
|
|
71
|
+
flex-basis: min-content;
|
|
72
|
+
}
|
|
73
|
+
@media only screen and (max-width: 400px) {
|
|
74
|
+
flex-basis: max-content;
|
|
75
|
+
flex-direction: column;
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
|
|
55
|
-
.
|
|
56
|
-
|
|
79
|
+
.benchmark-filter {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: row;
|
|
82
|
+
gap: 0.25em;
|
|
83
|
+
padding: 0.25em;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
.benchmark-filter label {
|
|
87
|
+
font-size: small;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.benchmark-filter select {
|
|
92
|
+
font-size: small;
|
|
93
|
+
border: 1px solid;
|
|
94
|
+
border-radius: 0.4em;
|
|
95
|
+
}
|
|
96
|
+
.benchmark-filter input[type="checkbox"] {
|
|
61
97
|
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { render, html } from "https://unpkg.com/uhtml@3.2.1?module";
|
|
2
|
+
import { fetchBenchmarkData } from "./benchmark-loader.js";
|
|
3
|
+
const KNOWN_PARAMS = [
|
|
4
|
+
"storage_type",
|
|
5
|
+
"limit",
|
|
6
|
+
"strategy",
|
|
7
|
+
"async",
|
|
8
|
+
"percentage_full",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function getBenchmarkData(result, query) {
|
|
12
|
+
let benchmarks = result.benchmarks;
|
|
13
|
+
return benchmarks.filter(function (benchmark) {
|
|
14
|
+
let okay = true;
|
|
15
|
+
if (query) {
|
|
16
|
+
Object.entries(query).forEach((entry) => {
|
|
17
|
+
let key = entry[0];
|
|
18
|
+
let value = entry[1];
|
|
19
|
+
if (
|
|
20
|
+
key != "group" &&
|
|
21
|
+
!(value === "") && // i.e. any.
|
|
22
|
+
benchmark.params[key] != null &&
|
|
23
|
+
benchmark.params[key] != value
|
|
24
|
+
) {
|
|
25
|
+
okay = false;
|
|
26
|
+
} else if (key == "group" && value != benchmark.group) {
|
|
27
|
+
okay = false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return okay;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatParam(key, str) {
|
|
36
|
+
if (key === "limit") {
|
|
37
|
+
var m = str.match(/(\d+(?:\.\d+)?)\s+per\s+1\s+(\w+)/i);
|
|
38
|
+
if (!m) return str;
|
|
39
|
+
var n = parseFloat(m[1]),
|
|
40
|
+
u = m[2].toLowerCase(),
|
|
41
|
+
num =
|
|
42
|
+
n >= 1000
|
|
43
|
+
? (n / 1000) % 1 === 0
|
|
44
|
+
? n / 1000 + "K"
|
|
45
|
+
: (n / 1000).toFixed(1) + "K"
|
|
46
|
+
: n.toString(),
|
|
47
|
+
umap = {
|
|
48
|
+
second: "s",
|
|
49
|
+
seconds: "s",
|
|
50
|
+
minute: "min",
|
|
51
|
+
minutes: "mins",
|
|
52
|
+
hour: "hr",
|
|
53
|
+
hours: "hr",
|
|
54
|
+
day: "day",
|
|
55
|
+
days: "day",
|
|
56
|
+
};
|
|
57
|
+
return num + "/" + (umap[u] || u);
|
|
58
|
+
} else if (key === "percentage_full") {
|
|
59
|
+
return `${str}% Seeded`;
|
|
60
|
+
}
|
|
61
|
+
return str;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nameTransform(benchmark, stripParams, query) {
|
|
65
|
+
let name = benchmark.name;
|
|
66
|
+
let params = benchmark.params;
|
|
67
|
+
name = name
|
|
68
|
+
.replace(/\[.*?\]/, "")
|
|
69
|
+
.replace("_async", "")
|
|
70
|
+
.replaceAll("_", "-");
|
|
71
|
+
name = name.replace(benchmark.group, "");
|
|
72
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
73
|
+
let additional = getRemainingGroups(benchmark, query);
|
|
74
|
+
Object.entries(additional).forEach((param) => {
|
|
75
|
+
let value = formatParam(param[0], param[1]);
|
|
76
|
+
if (name) {
|
|
77
|
+
name += ` - ${value}`;
|
|
78
|
+
} else {
|
|
79
|
+
name = `${value}`;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getRemainingGroups(benchmark, query) {
|
|
86
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
87
|
+
let additional = {};
|
|
88
|
+
Object.entries(benchmark.params).forEach((param) => {
|
|
89
|
+
const key = param[0];
|
|
90
|
+
const value = param[1];
|
|
91
|
+
if (
|
|
92
|
+
(!queryParam.includes(key) || query?.[key] === "") &&
|
|
93
|
+
KNOWN_PARAMS.includes(key)
|
|
94
|
+
) {
|
|
95
|
+
additional[key] = value;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return additional;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getColorForStorage(storageType) {
|
|
102
|
+
const storageColorMap = {
|
|
103
|
+
memory: window
|
|
104
|
+
.getComputedStyle(document.body)
|
|
105
|
+
.getPropertyValue("--color-purple"),
|
|
106
|
+
mongodb: window
|
|
107
|
+
.getComputedStyle(document.body)
|
|
108
|
+
.getPropertyValue("--color-yellow"),
|
|
109
|
+
memcached: window
|
|
110
|
+
.getComputedStyle(document.body)
|
|
111
|
+
.getPropertyValue("--color-aqua"),
|
|
112
|
+
redis: window
|
|
113
|
+
.getComputedStyle(document.body)
|
|
114
|
+
.getPropertyValue("--color-red"),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Fallback color if an unknown storageType appears
|
|
118
|
+
return storageColorMap[storageType] || "#7f7f7f"; // gray
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sortBenchmarksByParams(benchmarks, sortKeys) {
|
|
122
|
+
return benchmarks.sort(function (a, b) {
|
|
123
|
+
for (const key of sortKeys) {
|
|
124
|
+
let valA = (a.params?.[key] || "").toLowerCase();
|
|
125
|
+
let valB = (b.params?.[key] || "").toLowerCase();
|
|
126
|
+
if (key === "limit") {
|
|
127
|
+
valA = parseInt(valA.split(" ")[0], 10);
|
|
128
|
+
valB = parseInt(valB.split(" ")[0], 10);
|
|
129
|
+
}
|
|
130
|
+
if (valA < valB) return -1;
|
|
131
|
+
if (valA > valB) return 1;
|
|
132
|
+
}
|
|
133
|
+
return a.name.localeCompare(b.name);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let dispatched = new Set();
|
|
138
|
+
|
|
139
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
140
|
+
const charts = document.querySelectorAll(".benchmark-chart");
|
|
141
|
+
charts.forEach((chart) => {
|
|
142
|
+
const source = chart.dataset.source;
|
|
143
|
+
const filters = JSON.parse(chart.dataset.filters);
|
|
144
|
+
const query = JSON.parse(chart.dataset.query);
|
|
145
|
+
const paramMapping = JSON.parse(chart.dataset.paramMapping);
|
|
146
|
+
const chartId = chart.dataset.chartId;
|
|
147
|
+
let sortBy = JSON.parse(
|
|
148
|
+
chart.dataset.sortBy || '["storage_type", "limit"]',
|
|
149
|
+
);
|
|
150
|
+
render(
|
|
151
|
+
chart,
|
|
152
|
+
html`
|
|
153
|
+
<div class="benchmark-chart-loading">
|
|
154
|
+
<span>Loading</span>
|
|
155
|
+
</div>
|
|
156
|
+
`,
|
|
157
|
+
);
|
|
158
|
+
if (!dispatched.has(source)) {
|
|
159
|
+
fetchBenchmarkData(`${source}.json`)
|
|
160
|
+
.then((result) => {
|
|
161
|
+
window.Benchmarks[source] = result;
|
|
162
|
+
let event = new Event(`${source}-loaded`);
|
|
163
|
+
window.dispatchEvent(event);
|
|
164
|
+
})
|
|
165
|
+
.catch((error) => {
|
|
166
|
+
let event = new Event(`${source}-failed`);
|
|
167
|
+
window.dispatchEvent(event);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
dispatched.add(source);
|
|
171
|
+
window.addEventListener(`${chart.dataset.source}-failed`, function () {
|
|
172
|
+
chart.querySelector(".benchmark-chart-loading")?.remove();
|
|
173
|
+
render(
|
|
174
|
+
chart,
|
|
175
|
+
html`
|
|
176
|
+
<div class="benchmark-chart-error">Benchmark data not available.</div>
|
|
177
|
+
`,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
window.addEventListener(`${chart.dataset.source}-loaded`, function () {
|
|
181
|
+
chart.innerHTML = "";
|
|
182
|
+
chart.querySelector(".benchmark-chart-loading")?.remove();
|
|
183
|
+
const results = Benchmarks[chart.dataset.source];
|
|
184
|
+
const allBenchmarks = getBenchmarkData(results, query);
|
|
185
|
+
const currentFilters = Object.fromEntries(
|
|
186
|
+
Object.entries(filters).map(([key, value]) => {
|
|
187
|
+
return typeof value.default === "boolean"
|
|
188
|
+
? [key, value.default]
|
|
189
|
+
: [key, value.default != null ? value.default.toString() : ""];
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
const queryFilter = { ...query, ...currentFilters };
|
|
193
|
+
const dropdownTarget = document.createElement("div");
|
|
194
|
+
dropdownTarget.classList.add("benchmark-filters");
|
|
195
|
+
const chartTarget = document.createElement("div");
|
|
196
|
+
chart.append(chartTarget);
|
|
197
|
+
chart.append(dropdownTarget);
|
|
198
|
+
function renderDropdowns() {
|
|
199
|
+
const dropdowns = Object.entries(filters).map(([key]) => {
|
|
200
|
+
const fullName = `${chartId}-${key}`;
|
|
201
|
+
const uniqueValues = [
|
|
202
|
+
...new Set(allBenchmarks.map((b) => b.params?.[key])),
|
|
203
|
+
].sort();
|
|
204
|
+
const isBoolean =
|
|
205
|
+
uniqueValues.length === 2 &&
|
|
206
|
+
uniqueValues.includes(true) &&
|
|
207
|
+
uniqueValues.includes(false);
|
|
208
|
+
if (isBoolean) {
|
|
209
|
+
return html`
|
|
210
|
+
<div class="benchmark-filter" title=${paramMapping[key]?.info}>
|
|
211
|
+
<input
|
|
212
|
+
type="checkbox"
|
|
213
|
+
id=${fullName}
|
|
214
|
+
?checked=${currentFilters[key] === true}
|
|
215
|
+
onchange=${(e) => {
|
|
216
|
+
currentFilters[key] = e.target.checked;
|
|
217
|
+
renderChartWithFilters(currentFilters);
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
<label for=${fullName}>
|
|
221
|
+
${paramMapping[key]?.display || key}
|
|
222
|
+
</label>
|
|
223
|
+
</div>
|
|
224
|
+
`;
|
|
225
|
+
} else {
|
|
226
|
+
return html`
|
|
227
|
+
<div class="benchmark-filter" title=${paramMapping[key]?.info}>
|
|
228
|
+
<label for=${fullName}>
|
|
229
|
+
${paramMapping[key]?.display || key}
|
|
230
|
+
<select
|
|
231
|
+
id=${fullName}
|
|
232
|
+
onchange=${(e) => {
|
|
233
|
+
const value = e.target.value;
|
|
234
|
+
if (value) {
|
|
235
|
+
currentFilters[key] =
|
|
236
|
+
value === "false"
|
|
237
|
+
? false
|
|
238
|
+
: value === "true"
|
|
239
|
+
? true
|
|
240
|
+
: value;
|
|
241
|
+
} else {
|
|
242
|
+
currentFilters[key] = "";
|
|
243
|
+
}
|
|
244
|
+
renderChartWithFilters(currentFilters);
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<option value="" ?selected=${currentFilters[key] == ""}>
|
|
248
|
+
All
|
|
249
|
+
</option>
|
|
250
|
+
${uniqueValues.map(
|
|
251
|
+
(val) => html`
|
|
252
|
+
<option
|
|
253
|
+
value=${val}
|
|
254
|
+
?selected=${currentFilters[key] == val.toString()}
|
|
255
|
+
>
|
|
256
|
+
${val}
|
|
257
|
+
</option>
|
|
258
|
+
`,
|
|
259
|
+
)}
|
|
260
|
+
</select>
|
|
261
|
+
</label>
|
|
262
|
+
</div>
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
render(
|
|
267
|
+
dropdownTarget,
|
|
268
|
+
html`<div class="benchmark-filter-dropdowns">${dropdowns}</div>`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function legendKeyFunc(benchmark, key) {
|
|
272
|
+
return key === "group" ? benchmark.group : benchmark.params[key];
|
|
273
|
+
}
|
|
274
|
+
function renderChartWithFilters(currentFilters) {
|
|
275
|
+
const queryFilter = { ...query, ...currentFilters };
|
|
276
|
+
const data = sortBenchmarksByParams(
|
|
277
|
+
getBenchmarkData(results, queryFilter),
|
|
278
|
+
sortBy,
|
|
279
|
+
);
|
|
280
|
+
let legendGroupKey =
|
|
281
|
+
queryFilter?.storage_type == ""
|
|
282
|
+
? "storage_type"
|
|
283
|
+
: Object.entries(queryFilter).find((entry) => entry[1] === "")?.[0];
|
|
284
|
+
Plotly.newPlot(
|
|
285
|
+
chartTarget,
|
|
286
|
+
data.map((benchmark) => ({
|
|
287
|
+
type: "box",
|
|
288
|
+
name: nameTransform(benchmark, true, queryFilter),
|
|
289
|
+
y: benchmark.stats.data || [
|
|
290
|
+
benchmark.stats.min * 1e3,
|
|
291
|
+
benchmark.stats.q1 * 1e3,
|
|
292
|
+
benchmark.stats.median * 1e3,
|
|
293
|
+
benchmark.stats.q3 * 1e3,
|
|
294
|
+
benchmark.stats.max * 1e3,
|
|
295
|
+
],
|
|
296
|
+
boxmean: true,
|
|
297
|
+
boxpoints: false,
|
|
298
|
+
line: { width: 1 },
|
|
299
|
+
marker: {
|
|
300
|
+
color: getColorForStorage(benchmark.params.storage_type),
|
|
301
|
+
},
|
|
302
|
+
showlegend: true,
|
|
303
|
+
legendgroup: legendKeyFunc(benchmark, legendGroupKey),
|
|
304
|
+
legendgrouptitle: {
|
|
305
|
+
text: formatParam(
|
|
306
|
+
legendGroupKey,
|
|
307
|
+
legendKeyFunc(benchmark, legendGroupKey),
|
|
308
|
+
),
|
|
309
|
+
},
|
|
310
|
+
})),
|
|
311
|
+
{
|
|
312
|
+
yaxis: {
|
|
313
|
+
title: { text: "Time (ms)" },
|
|
314
|
+
exponentformat: "none",
|
|
315
|
+
ticksuffix: " ms",
|
|
316
|
+
tickformat: ",.2f",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
responsive: true,
|
|
321
|
+
displaylogo: false,
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
renderDropdowns();
|
|
327
|
+
renderChartWithFilters(currentFilters);
|
|
328
|
+
let initial = true;
|
|
329
|
+
chartTarget.on("plotly_afterplot", function () {
|
|
330
|
+
const { hash } = window.location;
|
|
331
|
+
if (hash && initial) {
|
|
332
|
+
initial = false;
|
|
333
|
+
const target = document.querySelector(hash);
|
|
334
|
+
if (target) {
|
|
335
|
+
setTimeout(function () {
|
|
336
|
+
target.scrollIntoView({ behavior: "instant" });
|
|
337
|
+
}, 10);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import uuid
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from docutils import nodes
|
|
@@ -31,13 +32,14 @@ def query(argument):
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def filters(argument):
|
|
34
|
-
filters: dict[str,
|
|
35
|
+
filters: dict[str, dict[str, str | bool]] = {}
|
|
35
36
|
for filter in argument.strip().split(","):
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
source, default = filter.split("=")
|
|
38
|
+
if ":" in source:
|
|
39
|
+
source, label = source.split(":")
|
|
39
40
|
else:
|
|
40
|
-
|
|
41
|
+
source, label = source, source
|
|
42
|
+
filters[source] = {"label": label, "default": check_bool(default)}
|
|
41
43
|
return filters
|
|
42
44
|
|
|
43
45
|
|
|
@@ -66,7 +68,6 @@ class BenchmarkDetails(SphinxDirective):
|
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
class BenchmarkChart(SphinxDirective):
|
|
69
|
-
required_arguments = 0
|
|
70
71
|
final_argument_whitespace = False
|
|
71
72
|
option_spec = {
|
|
72
73
|
"source": str,
|
|
@@ -78,17 +79,18 @@ class BenchmarkChart(SphinxDirective):
|
|
|
78
79
|
|
|
79
80
|
def run(self):
|
|
80
81
|
source = self.options.get("source", "benchmark-summary")
|
|
81
|
-
filters = self.options.get("filters",
|
|
82
|
+
filters = self.options.get("filters", {})
|
|
82
83
|
query = self.options.get("query", {})
|
|
83
84
|
sortBy = self.options.get("sort", [])
|
|
84
|
-
|
|
85
85
|
html = f"""
|
|
86
86
|
<div
|
|
87
87
|
class='benchmark-chart'
|
|
88
88
|
data-source='{source}'
|
|
89
89
|
data-filters='{json.dumps(filters)}'
|
|
90
90
|
data-query='{json.dumps(query)}'
|
|
91
|
-
data-
|
|
91
|
+
data-sort-by='{json.dumps(sortBy)}'
|
|
92
|
+
data-chart-id='{uuid.uuid4().hex}'
|
|
93
|
+
data-param-mapping='{json.dumps(self.env.config.benchmark_param_mapping)}'>
|
|
92
94
|
</div>
|
|
93
95
|
"""
|
|
94
96
|
|
|
@@ -117,6 +119,7 @@ def setup(app: Sphinx):
|
|
|
117
119
|
app.add_directive("benchmark-chart", BenchmarkChart)
|
|
118
120
|
app.add_directive("benchmark-details", BenchmarkDetails)
|
|
119
121
|
app.add_config_value("benchmark_git_context", default={}, rebuild="env")
|
|
122
|
+
app.add_config_value("benchmark_param_mapping", default={}, rebuild="env")
|
|
120
123
|
|
|
121
124
|
def add_assets(app, env) -> None:
|
|
122
125
|
static_path = os.path.join(here, "_static")
|
|
@@ -0,0 +1,126 @@
|
|
|
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 giving
|
|
10
|
+
a high level overview of the performance.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
.. dropdown:: Benchmark parameters
|
|
14
|
+
|
|
15
|
+
- 100 unique virtual users (i.e. unique rate limit keys)
|
|
16
|
+
- A rate limit of ``500/minute``
|
|
17
|
+
- Each virtual user's limit was pre-seeded to be 50% full.
|
|
18
|
+
|
|
19
|
+
See :ref:`performance:benchmark run details` for information on the benchmarking
|
|
20
|
+
environment.
|
|
21
|
+
|
|
22
|
+
.. tab-set::
|
|
23
|
+
|
|
24
|
+
.. tab-item:: Hit
|
|
25
|
+
|
|
26
|
+
Performance of :meth:`~limits.strategies.RateLimiter.hit`
|
|
27
|
+
by storage & strategy.
|
|
28
|
+
|
|
29
|
+
.. benchmark-chart::
|
|
30
|
+
:source: benchmark-summary
|
|
31
|
+
:query: limit=500 per 1 minute,group=hit,percentage_full=50
|
|
32
|
+
:sort: storage_type,strategy
|
|
33
|
+
:filters: storage_type=,strategy=,async=false
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
.. tab-item:: Test
|
|
37
|
+
|
|
38
|
+
Performance of :meth:`~limits.strategies.RateLimiter.test`
|
|
39
|
+
by storage & strategy.
|
|
40
|
+
|
|
41
|
+
.. benchmark-chart::
|
|
42
|
+
:source: benchmark-summary
|
|
43
|
+
:query: limit=500 per 1 minute,group=test,percentage_full=50
|
|
44
|
+
:sort: storage_type,strategy
|
|
45
|
+
:filters: storage_type=,strategy=,async=false
|
|
46
|
+
|
|
47
|
+
.. tab-item:: Get Window Stats
|
|
48
|
+
|
|
49
|
+
Performance of :meth:`~limits.strategies.RateLimiter.get_window_stats`
|
|
50
|
+
by storage & strategy.
|
|
51
|
+
|
|
52
|
+
.. benchmark-chart::
|
|
53
|
+
:source: benchmark-summary
|
|
54
|
+
:query: limit=500 per 1 minute,group=get-window-stats,percentage_full=50
|
|
55
|
+
:sort: storage_type,strategy
|
|
56
|
+
:filters: storage_type=,strategy=,async=false
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Performance implication of limit sizes
|
|
60
|
+
--------------------------------------
|
|
61
|
+
|
|
62
|
+
Though for :ref:`strategies:fixed window` and :ref:`strategies:sliding window counter` both the
|
|
63
|
+
storage cost and performance of operations remains constant when the limit window and size varies,
|
|
64
|
+
this is not true for :ref:`strategies:moving window` which maintains a complete log of successful
|
|
65
|
+
requests within the window.
|
|
66
|
+
|
|
67
|
+
The following benchmarks demonstrate the implications when using various limits.
|
|
68
|
+
|
|
69
|
+
.. dropdown:: Benchmark parameters
|
|
70
|
+
|
|
71
|
+
- 100 unique virtual users
|
|
72
|
+
- Rate limits of
|
|
73
|
+
|
|
74
|
+
- ``500/minute``
|
|
75
|
+
- ``10000/day``
|
|
76
|
+
- ``100000/day``
|
|
77
|
+
- Each virtual user's limit was pre-seeded to be:
|
|
78
|
+
|
|
79
|
+
- 5% full.
|
|
80
|
+
- 50% full.
|
|
81
|
+
- 90% full.
|
|
82
|
+
|
|
83
|
+
See :ref:`performance:benchmark run details` for information on the benchmarking
|
|
84
|
+
environment.
|
|
85
|
+
|
|
86
|
+
.. tab-set::
|
|
87
|
+
|
|
88
|
+
.. tab-item:: Hit
|
|
89
|
+
|
|
90
|
+
Performance of :meth:`~limits.strategies.RateLimiter.hit`
|
|
91
|
+
with various rate limits
|
|
92
|
+
|
|
93
|
+
.. benchmark-chart::
|
|
94
|
+
:source: benchmark-summary
|
|
95
|
+
:query: group=hit
|
|
96
|
+
:sort: storage_type,limit
|
|
97
|
+
:filters: strategy=,percentage_full=50,storage_type=,async=false
|
|
98
|
+
|
|
99
|
+
.. tab-item:: Test
|
|
100
|
+
|
|
101
|
+
Performance of :meth:`~limits.strategies.RateLimiter.test`
|
|
102
|
+
with various rate limits
|
|
103
|
+
|
|
104
|
+
.. benchmark-chart::
|
|
105
|
+
:source: benchmark-summary
|
|
106
|
+
:query: group=test
|
|
107
|
+
:sort: storage_type,limit
|
|
108
|
+
:filters: strategy=,percentage_full=50,storage_type=,async=false
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
.. tab-item:: Get Window Stats
|
|
112
|
+
|
|
113
|
+
Performance of :meth:`~limits.strategies.RateLimiter.get_window_stats`
|
|
114
|
+
with various rate limits
|
|
115
|
+
|
|
116
|
+
.. benchmark-chart::
|
|
117
|
+
:source: benchmark-summary
|
|
118
|
+
:query: group=get-window-stats
|
|
119
|
+
:sort: storage_type,limit
|
|
120
|
+
:filters: strategy=,percentage_full=50,storage_type=,async=false
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
Benchmark run details
|
|
124
|
+
---------------------
|
|
125
|
+
.. benchmark-details::
|
|
126
|
+
:source: benchmark-summary
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-04-
|
|
11
|
+
"date": "2025-04-12T18:43:47-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "4.7.
|
|
14
|
+
"full-revisionid": "58af4445a2d0b1de9251cd60e847d25267b4830d",
|
|
15
|
+
"version": "4.7.3"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -93,7 +93,10 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
93
93
|
"""
|
|
94
94
|
:param key: the key to clear rate limits for
|
|
95
95
|
"""
|
|
96
|
-
|
|
96
|
+
try:
|
|
97
|
+
await (await self.get_storage()).delete(key.encode("utf-8"))
|
|
98
|
+
except self.dependency.NotFoundCommandError:
|
|
99
|
+
pass
|
|
97
100
|
|
|
98
101
|
async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
|
|
99
102
|
"""
|