limits 5.0.0rc1__tar.gz → 5.1.0__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-5.0.0rc1 → limits-5.1.0}/HISTORY.rst +33 -6
- {limits-5.0.0rc1 → limits-5.1.0}/PKG-INFO +3 -2
- {limits-5.0.0rc1 → limits-5.1.0}/README.rst +1 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/conf.py +12 -1
- limits-5.1.0/doc/source/ext/_static/benchmark-chart.css +94 -0
- limits-5.1.0/doc/source/ext/_static/js/benchmark-chart.js +489 -0
- limits-5.1.0/doc/source/ext/_static/js/benchmark-details.js +117 -0
- limits-5.1.0/doc/source/ext/_static/js/benchmark-loader.js +43 -0
- limits-5.1.0/doc/source/ext/_templates/git_info.js +22 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/ext/bench_chart.py +15 -9
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/installation.rst +6 -1
- limits-5.1.0/doc/source/performance.rst +219 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/theme_config.py +10 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/_version.py +3 -3
- limits-5.0.0rc1/limits/aio/storage/memcached.py → limits-5.1.0/limits/aio/storage/memcached/__init__.py +43 -113
- limits-5.1.0/limits/aio/storage/memcached/bridge.py +73 -0
- limits-5.1.0/limits/aio/storage/memcached/emcache.py +112 -0
- limits-5.1.0/limits/aio/storage/memcached/memcachio.py +104 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/memory.py +29 -18
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/__init__.py +22 -6
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/bridge.py +3 -2
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/memcached.py +2 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/memory.py +5 -5
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis.py +5 -2
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis_cluster.py +5 -2
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis_sentinel.py +3 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/typing.py +1 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/PKG-INFO +3 -2
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/SOURCES.txt +4 -1
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/requires.txt +1 -1
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/docs.txt +3 -2
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/main.txt +1 -1
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/test.txt +1 -1
- {limits-5.0.0rc1 → limits-5.1.0}/setup.cfg +3 -0
- limits-5.0.0rc1/doc/source/ext/_static/benchmark-chart.css +0 -61
- limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-chart.js +0 -219
- limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-details.js +0 -103
- limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-loader.js +0 -31
- limits-5.0.0rc1/doc/source/ext/_templates/git_info.js +0 -2
- limits-5.0.0rc1/doc/source/performance.rst +0 -221
- {limits-5.0.0rc1 → limits-5.1.0}/CLASSIFIERS +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/CONTRIBUTIONS.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/LICENSE.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/MANIFEST.in +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/Makefile +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/_static/custom.css +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/api.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/async.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/changelog.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/custom-storage.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/index.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/quickstart.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/storage.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/doc/source/strategies.rst +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/__init__.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/__init__.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/__init__.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/base.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/mongodb.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/coredis.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/redispy.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/valkey.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/strategies.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/errors.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/limits.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/py.typed +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/__init__.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/base.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/mongodb.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/registry.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/strategies.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/util.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits/version.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/dependency_links.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/not-zip-safe +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/top_level.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/pyproject.toml +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/ci.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/dev.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-memcached.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-mongodb.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-redis.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-valkey.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/memcached.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/mongodb.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/redis.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/rediscluster.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/valkey.txt +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/setup.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_limit_granularities.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_limits.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_ratelimit_parser.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_storage.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_strategy.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/tests/test_utils.py +0 -0
- {limits-5.0.0rc1 → limits-5.1.0}/versioneer.py +0 -0
|
@@ -3,21 +3,46 @@
|
|
|
3
3
|
Changelog
|
|
4
4
|
=========
|
|
5
5
|
|
|
6
|
-
v5.0
|
|
7
|
-
|
|
8
|
-
Release Date: 2025-04-
|
|
6
|
+
v5.1.0
|
|
7
|
+
------
|
|
8
|
+
Release Date: 2025-04-23
|
|
9
|
+
|
|
10
|
+
* Features
|
|
11
|
+
|
|
12
|
+
* Expose ``key_prefix`` constructor argument for all redis storage
|
|
13
|
+
implementations to simplify customizing the prefix used for all
|
|
14
|
+
keys created in redis.
|
|
15
|
+
|
|
16
|
+
v5.0.0
|
|
17
|
+
------
|
|
18
|
+
Release Date: 2025-04-15
|
|
9
19
|
|
|
10
20
|
* Backward incompatible changes
|
|
11
21
|
|
|
12
22
|
* Dropped support for Fixed Window with Elastic Expiry strategy
|
|
13
23
|
* Dropped support for etcd
|
|
14
|
-
*
|
|
24
|
+
* Changed the default implementation for async+memached from :pypi:`emcache`
|
|
25
|
+
to :pypi`:memcachio`
|
|
15
26
|
|
|
16
27
|
* Performance
|
|
17
28
|
|
|
18
|
-
* Improved performance of
|
|
19
|
-
|
|
29
|
+
* Improved performance of redis moving window ``test`` and ``get_window_stats`` operations
|
|
30
|
+
especially when dealing with large rate limits.
|
|
20
31
|
* Improved performance of mongodb moving window ``test`` and ``get_window_stats`` operations.
|
|
32
|
+
* Improved performance of in-memory moving window ``test`` and ``get_window_stats`` operations.
|
|
33
|
+
* Reduced load on event loop when expiring limits with async in-memory implementations
|
|
34
|
+
|
|
35
|
+
v4.7.3
|
|
36
|
+
------
|
|
37
|
+
Release Date: 2025-04-12
|
|
38
|
+
|
|
39
|
+
* Documentation
|
|
40
|
+
|
|
41
|
+
* Expand benchmark results to included preseeded limits
|
|
42
|
+
|
|
43
|
+
* Bug Fix
|
|
44
|
+
|
|
45
|
+
* Handle clearing missing key with memcache + async
|
|
21
46
|
|
|
22
47
|
v4.7.2
|
|
23
48
|
------
|
|
@@ -809,3 +834,5 @@ Release Date: 2015-01-08
|
|
|
809
834
|
|
|
810
835
|
* Initial import of common rate limiting code from `Flask-Limiter <https://github.com/alisaifee/flask-limiter>`_
|
|
811
836
|
|
|
837
|
+
|
|
838
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: limits
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
23
|
License-File: LICENSE.txt
|
|
24
24
|
Requires-Dist: deprecated>=1.2
|
|
25
|
-
Requires-Dist: packaging<
|
|
25
|
+
Requires-Dist: packaging<26,>=21
|
|
26
26
|
Requires-Dist: typing_extensions
|
|
27
27
|
Provides-Extra: redis
|
|
28
28
|
Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "redis"
|
|
@@ -274,5 +274,6 @@ Links
|
|
|
274
274
|
=====
|
|
275
275
|
|
|
276
276
|
* `Documentation <http://limits.readthedocs.org/en/latest>`_
|
|
277
|
+
* `Benchmarks <http://limits.readthedocs.org/en/latest/performance.html>`_
|
|
277
278
|
* `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
|
|
278
279
|
|
|
@@ -16,10 +16,12 @@ project = "limits"
|
|
|
16
16
|
description = "limits is a python library to perform rate limiting with commonly used storage backends"
|
|
17
17
|
copyright = "2023, Ali-Akber Saifee"
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
if ".post0.dev" in limits.__version__:
|
|
20
21
|
version, ahead = limits.__version__.split(".post0.dev")
|
|
21
22
|
else:
|
|
22
23
|
version = limits.__version__
|
|
24
|
+
ahead = 0
|
|
23
25
|
|
|
24
26
|
release = version
|
|
25
27
|
|
|
@@ -38,7 +40,15 @@ else:
|
|
|
38
40
|
"branch": git_info.get("branch", ""),
|
|
39
41
|
"sha": git_info.get("long", None),
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
benchmark_param_mapping = {
|
|
44
|
+
"percentage_full": {
|
|
45
|
+
"display": "Percentage Seeded",
|
|
46
|
+
"info": "Percentage of rate limit already filled before the benchmark is run",
|
|
47
|
+
},
|
|
48
|
+
"async": {"display": "Asyncio", "info": "Using asyncio storage implementation"},
|
|
49
|
+
"storage_type": {"display": "Storage", "info": "Storage Backend"},
|
|
50
|
+
"strategy": {"display": "Strategy", "info": "Rate Limiting Strategy"},
|
|
51
|
+
}
|
|
42
52
|
html_static_path = ["_static"]
|
|
43
53
|
|
|
44
54
|
html_css_files = [
|
|
@@ -73,6 +83,7 @@ extensions = [
|
|
|
73
83
|
"sphinxext.opengraph",
|
|
74
84
|
"sphinxcontrib.programoutput",
|
|
75
85
|
"sphinx_copybutton",
|
|
86
|
+
"sphinx_design",
|
|
76
87
|
"sphinx_inline_tabs",
|
|
77
88
|
"sphinx_paramlinks",
|
|
78
89
|
"bench_chart",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.benchmark-chart {
|
|
2
|
+
width: 100%;
|
|
3
|
+
}
|
|
4
|
+
.benchmark-details {
|
|
5
|
+
.benchmark-details-section {
|
|
6
|
+
thead > tr {
|
|
7
|
+
color: var(--color-purple);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
.benchmark-chart-error,
|
|
12
|
+
.benchmark-details-error {
|
|
13
|
+
color: var(--color-red);
|
|
14
|
+
font-size: smaller;
|
|
15
|
+
padding: 2em;
|
|
16
|
+
text-align: center;
|
|
17
|
+
height: 5em;
|
|
18
|
+
}
|
|
19
|
+
.benchmark-chart-loading {
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
align-items: center;
|
|
23
|
+
font-size: 1.2rem;
|
|
24
|
+
font-weight: bold;
|
|
25
|
+
color: var(--color-purple);
|
|
26
|
+
letter-spacing: 1px;
|
|
27
|
+
height: 200px;
|
|
28
|
+
animation: benchmark-chart-loading-animation;
|
|
29
|
+
animation-iteration-count: 10;
|
|
30
|
+
animation-duration: 2s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes benchmark-chart-loading-animation {
|
|
34
|
+
0% {
|
|
35
|
+
opacity: 0;
|
|
36
|
+
}
|
|
37
|
+
25% {
|
|
38
|
+
opacity: 0.25;
|
|
39
|
+
}
|
|
40
|
+
50% {
|
|
41
|
+
opacity: 0.5;
|
|
42
|
+
}
|
|
43
|
+
90% {
|
|
44
|
+
opacity: 0.9;
|
|
45
|
+
}
|
|
46
|
+
100% {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
.benchmark-filters {
|
|
51
|
+
padding-top: 1em;
|
|
52
|
+
padding-bottom: 1em;
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: flex-end;
|
|
55
|
+
}
|
|
56
|
+
.benchmark-filter-dropdowns {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: flex-end;
|
|
59
|
+
justify-content: flex-end;
|
|
60
|
+
margin-top: 2em;
|
|
61
|
+
flex-direction: row;
|
|
62
|
+
@media only screen and (max-width: 800px) {
|
|
63
|
+
flex-basis: min-content;
|
|
64
|
+
}
|
|
65
|
+
@media only screen and (max-width: 400px) {
|
|
66
|
+
flex-basis: max-content;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.compare-dropdown {
|
|
72
|
+
display: flex;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.benchmark-filter {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: row;
|
|
79
|
+
gap: 0.25em;
|
|
80
|
+
padding: 0.25em;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.benchmark-filter label,
|
|
84
|
+
.compare-filter label {
|
|
85
|
+
font-size: small;
|
|
86
|
+
font-weight: bold;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.benchmark-filter select,
|
|
90
|
+
.compare-filter select {
|
|
91
|
+
font-size: small;
|
|
92
|
+
border: 1px solid;
|
|
93
|
+
border-radius: 0.4em;
|
|
94
|
+
}
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { render, html } from "https://unpkg.com/uhtml@3.2.1?module";
|
|
2
|
+
import { BenchmarkLoader } from "./benchmark-loader.js";
|
|
3
|
+
const KNOWN_PARAMS = [
|
|
4
|
+
"storage_type",
|
|
5
|
+
"limit",
|
|
6
|
+
"strategy",
|
|
7
|
+
"async",
|
|
8
|
+
"percentage_full",
|
|
9
|
+
];
|
|
10
|
+
let dispatched = new Set();
|
|
11
|
+
window.Benchmarks = {};
|
|
12
|
+
let currentLoader = new BenchmarkLoader();
|
|
13
|
+
let compareLoaders = {};
|
|
14
|
+
|
|
15
|
+
class Utils {
|
|
16
|
+
static getBenchmarkData(result, query) {
|
|
17
|
+
let benchmarks = result.benchmarks;
|
|
18
|
+
return benchmarks.filter(function (benchmark) {
|
|
19
|
+
let okay = true;
|
|
20
|
+
if (query) {
|
|
21
|
+
Object.entries(query).forEach((entry) => {
|
|
22
|
+
let key = entry[0];
|
|
23
|
+
let value = entry[1];
|
|
24
|
+
if (
|
|
25
|
+
key != "group" &&
|
|
26
|
+
!(value === "") && // i.e. any.
|
|
27
|
+
benchmark.params[key] != null &&
|
|
28
|
+
benchmark.params[key] != value
|
|
29
|
+
) {
|
|
30
|
+
okay = false;
|
|
31
|
+
} else if (key == "group" && value != benchmark.group) {
|
|
32
|
+
okay = false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return okay;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static formatParam(key, str) {
|
|
41
|
+
if (key === "limit") {
|
|
42
|
+
var m = str.match(/(\d+(?:\.\d+)?)\s+per\s+1\s+(\w+)/i);
|
|
43
|
+
if (!m) return str;
|
|
44
|
+
var n = parseFloat(m[1]),
|
|
45
|
+
u = m[2].toLowerCase(),
|
|
46
|
+
num =
|
|
47
|
+
n >= 1000
|
|
48
|
+
? (n / 1000) % 1 === 0
|
|
49
|
+
? n / 1000 + "K"
|
|
50
|
+
: (n / 1000).toFixed(1) + "K"
|
|
51
|
+
: n.toString(),
|
|
52
|
+
umap = {
|
|
53
|
+
second: "s",
|
|
54
|
+
seconds: "s",
|
|
55
|
+
minute: "min",
|
|
56
|
+
minutes: "mins",
|
|
57
|
+
hour: "hr",
|
|
58
|
+
hours: "hr",
|
|
59
|
+
day: "day",
|
|
60
|
+
days: "day",
|
|
61
|
+
};
|
|
62
|
+
return num + "/" + (umap[u] || u);
|
|
63
|
+
} else if (key === "percentage_full") {
|
|
64
|
+
return `${str}% Seeded`;
|
|
65
|
+
} else if (key === "strategy") {
|
|
66
|
+
return str.replaceAll("-", " ").replace(/\b\w/g, (s) => s.toUpperCase());
|
|
67
|
+
} else if (key === "async") {
|
|
68
|
+
return str == true ? "asyncio" : "sync";
|
|
69
|
+
}
|
|
70
|
+
return str;
|
|
71
|
+
}
|
|
72
|
+
static nameTransform(benchmark, stripParams, query) {
|
|
73
|
+
let name = benchmark.name;
|
|
74
|
+
let params = benchmark.params;
|
|
75
|
+
name = name
|
|
76
|
+
.replace(/\[.*?\]/, "")
|
|
77
|
+
.replace("_async", "")
|
|
78
|
+
.replaceAll("_", "-");
|
|
79
|
+
name = name.replace(benchmark.group, "");
|
|
80
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
81
|
+
let additional = Utils.getRemainingGroups(benchmark, query);
|
|
82
|
+
Object.entries(additional).forEach((param) => {
|
|
83
|
+
let value = Utils.formatParam(param[0], param[1]);
|
|
84
|
+
if (name) {
|
|
85
|
+
name += ` - ${value}`;
|
|
86
|
+
} else {
|
|
87
|
+
name = `${value}`;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static getRemainingGroups(benchmark, query) {
|
|
94
|
+
let queryParam = Object.entries(query).map((entry) => entry[0]);
|
|
95
|
+
let additional = {};
|
|
96
|
+
Object.entries(benchmark.params).forEach((param) => {
|
|
97
|
+
const key = param[0];
|
|
98
|
+
const value = param[1];
|
|
99
|
+
if (
|
|
100
|
+
(!queryParam.includes(key) || query?.[key] === "") &&
|
|
101
|
+
KNOWN_PARAMS.includes(key)
|
|
102
|
+
) {
|
|
103
|
+
additional[key] = value;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return additional;
|
|
107
|
+
}
|
|
108
|
+
static color(name) {
|
|
109
|
+
return window
|
|
110
|
+
.getComputedStyle(document.body)
|
|
111
|
+
.getPropertyValue(`--color-${name}`);
|
|
112
|
+
}
|
|
113
|
+
static getColorForStorage(storageType) {
|
|
114
|
+
const storageColorMap = {
|
|
115
|
+
memory: this.color("purple"),
|
|
116
|
+
mongodb: this.color("mongodb"),
|
|
117
|
+
memcached: this.color("blue"),
|
|
118
|
+
redis: this.color("redis"),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return storageColorMap[storageType] || "#7f7f7f"; // gray
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static sortBenchmarksByParams(benchmarks, sortKeys) {
|
|
125
|
+
return benchmarks.sort(function (a, b) {
|
|
126
|
+
for (const key of sortKeys) {
|
|
127
|
+
let valA = (a.params?.[key] || "").toLowerCase();
|
|
128
|
+
let valB = (b.params?.[key] || "").toLowerCase();
|
|
129
|
+
if (key === "limit") {
|
|
130
|
+
valA = parseInt(valA.split(" ")[0], 10);
|
|
131
|
+
valB = parseInt(valB.split(" ")[0], 10);
|
|
132
|
+
}
|
|
133
|
+
if (valA < valB) return -1;
|
|
134
|
+
if (valA > valB) return 1;
|
|
135
|
+
}
|
|
136
|
+
return a.name.localeCompare(b.name);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static fetchComparisonData(source, comparisonSource) {
|
|
141
|
+
if (!compareLoaders[comparisonSource]) {
|
|
142
|
+
compareLoaders[comparisonSource] = new BenchmarkLoader([
|
|
143
|
+
`https://${comparisonSource.replaceAll(".", "-")}--py-limits.netlify.app/`,
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
return compareLoaders[comparisonSource].fetchBenchmarkData(source);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class BenchmarkChart {
|
|
151
|
+
constructor(node) {
|
|
152
|
+
this.chart = node;
|
|
153
|
+
this.title = this.chart.dataset.title;
|
|
154
|
+
this.source = this.chart.dataset.source;
|
|
155
|
+
this.filters = JSON.parse(this.chart.dataset.filters);
|
|
156
|
+
this.query = JSON.parse(this.chart.dataset.query);
|
|
157
|
+
this.paramMapping = JSON.parse(this.chart.dataset.paramMapping);
|
|
158
|
+
this.chartId = this.chart.dataset.chartId;
|
|
159
|
+
this.currentCompare = null;
|
|
160
|
+
this.sortBy = JSON.parse(
|
|
161
|
+
this.chart.dataset.sortBy || '["storage_type", "limit"]',
|
|
162
|
+
);
|
|
163
|
+
let self = this;
|
|
164
|
+
render(
|
|
165
|
+
this.chart,
|
|
166
|
+
html`
|
|
167
|
+
<div class="benchmark-chart-loading">
|
|
168
|
+
<span>Loading</span>
|
|
169
|
+
</div>
|
|
170
|
+
`,
|
|
171
|
+
);
|
|
172
|
+
if (!dispatched.has(this.source)) {
|
|
173
|
+
currentLoader
|
|
174
|
+
.fetchBenchmarkData(this.source)
|
|
175
|
+
.then((result) => {
|
|
176
|
+
let event = new Event(`${self.source}-loaded`);
|
|
177
|
+
window.dispatchEvent(event);
|
|
178
|
+
})
|
|
179
|
+
.catch((error) => {
|
|
180
|
+
let event = new Event(`${self.source}-failed`);
|
|
181
|
+
window.dispatchEvent(event);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
dispatched.add(this.source);
|
|
185
|
+
window.addEventListener(`${this.source}-failed`, function () {
|
|
186
|
+
self.handleError();
|
|
187
|
+
});
|
|
188
|
+
window.addEventListener(`${this.source}-loaded`, function () {
|
|
189
|
+
self.render();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
handleError() {
|
|
193
|
+
this.chart.querySelector(".benchmark-chart-loading")?.remove();
|
|
194
|
+
render(
|
|
195
|
+
this.chart,
|
|
196
|
+
html`
|
|
197
|
+
<div class="benchmark-chart-error">Benchmark data not available.</div>
|
|
198
|
+
`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
chartTitle(query) {
|
|
202
|
+
let selectedFilters = Object.entries(query).filter(
|
|
203
|
+
(entry) =>
|
|
204
|
+
entry[1] != null &&
|
|
205
|
+
entry[1] != "" &&
|
|
206
|
+
!this.query.hasOwnProperty(entry[0]),
|
|
207
|
+
);
|
|
208
|
+
let derivedTitle = selectedFilters.reduce(
|
|
209
|
+
(title, entry) =>
|
|
210
|
+
(title += `${title == "" ? "" : ", "}${Utils.formatParam(entry[0], entry[1])}`),
|
|
211
|
+
"",
|
|
212
|
+
);
|
|
213
|
+
if (this.title != "") {
|
|
214
|
+
return `${this.title}<br>${derivedTitle}`;
|
|
215
|
+
} else {
|
|
216
|
+
return derivedTitle;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
renderCompare() {
|
|
220
|
+
const compareTargets = ["master", "stable", "4.x"]
|
|
221
|
+
.concat(window.LATEST_RELEASES || [])
|
|
222
|
+
.filter((element) => element != window.GITBRANCH);
|
|
223
|
+
let self = this;
|
|
224
|
+
const compareDropdown = html`
|
|
225
|
+
<div
|
|
226
|
+
class="compare-filter"
|
|
227
|
+
title="Compare against another release or branch"
|
|
228
|
+
>
|
|
229
|
+
<label>
|
|
230
|
+
Compare with
|
|
231
|
+
<select
|
|
232
|
+
onchange=${(e) => {
|
|
233
|
+
const value = e.target.value;
|
|
234
|
+
self.currentCompare = value;
|
|
235
|
+
|
|
236
|
+
if (value !== "") {
|
|
237
|
+
Utils.fetchComparisonData(self.source, value).then(
|
|
238
|
+
function (comparisonData) {
|
|
239
|
+
self.currentComparisonData = comparisonData;
|
|
240
|
+
self.renderChartWithFilters();
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
self.currentComparisonData = null;
|
|
245
|
+
self.renderChartWithFilters();
|
|
246
|
+
}
|
|
247
|
+
let event = new Event("benchmark-compare-changed");
|
|
248
|
+
event.compare = self.currentCompare;
|
|
249
|
+
event.source = self;
|
|
250
|
+
window.dispatchEvent(event);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<option value=""></option>
|
|
254
|
+
${compareTargets.map(
|
|
255
|
+
(val) => html`
|
|
256
|
+
<option
|
|
257
|
+
value=${val}
|
|
258
|
+
?selected=${this.currentCompare == val.toString()}
|
|
259
|
+
>
|
|
260
|
+
${val}
|
|
261
|
+
</option>
|
|
262
|
+
`,
|
|
263
|
+
)}
|
|
264
|
+
</select>
|
|
265
|
+
</label>
|
|
266
|
+
</div>
|
|
267
|
+
`;
|
|
268
|
+
window.addEventListener("benchmark-compare-changed", function (event) {
|
|
269
|
+
let select = self.compareTarget.getElementsByTagName("select")[0];
|
|
270
|
+
if (event.source != self && select.value != event.compare) {
|
|
271
|
+
select.value = event.compare;
|
|
272
|
+
select.dispatchEvent(new Event("change"));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
render(
|
|
276
|
+
this.compareTarget,
|
|
277
|
+
html`<div class="compare-dropdown">${compareDropdown}</div>`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
renderDropdowns() {
|
|
281
|
+
const dropdowns = Object.entries(this.filters).map(([key]) => {
|
|
282
|
+
const fullName = `${this.chartId}-${key}`;
|
|
283
|
+
const uniqueValues = [
|
|
284
|
+
...new Set(this.allBenchmarks.map((b) => b.params?.[key])),
|
|
285
|
+
].sort();
|
|
286
|
+
const isBoolean =
|
|
287
|
+
uniqueValues.length === 2 &&
|
|
288
|
+
uniqueValues.includes(true) &&
|
|
289
|
+
uniqueValues.includes(false);
|
|
290
|
+
let self = this;
|
|
291
|
+
if (isBoolean) {
|
|
292
|
+
return html`
|
|
293
|
+
<div class="benchmark-filter" title=${this.paramMapping[key]?.info}>
|
|
294
|
+
<input
|
|
295
|
+
type="checkbox"
|
|
296
|
+
id=${fullName}
|
|
297
|
+
?checked=${self.currentFilters[key] === true}
|
|
298
|
+
onchange=${(e) => {
|
|
299
|
+
self.currentFilters[key] = e.target.checked;
|
|
300
|
+
self.renderChartWithFilters();
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
<label for=${fullName}>
|
|
304
|
+
${self.paramMapping[key]?.display || key}
|
|
305
|
+
</label>
|
|
306
|
+
</div>
|
|
307
|
+
`;
|
|
308
|
+
} else {
|
|
309
|
+
return html`
|
|
310
|
+
<div class="benchmark-filter" title=${self.paramMapping[key]?.info}>
|
|
311
|
+
<label for=${fullName}>
|
|
312
|
+
${self.paramMapping[key]?.display || key}
|
|
313
|
+
<select
|
|
314
|
+
id=${fullName}
|
|
315
|
+
onchange=${(e) => {
|
|
316
|
+
const value = e.target.value;
|
|
317
|
+
if (value) {
|
|
318
|
+
self.currentFilters[key] =
|
|
319
|
+
value === "false"
|
|
320
|
+
? false
|
|
321
|
+
: value === "true"
|
|
322
|
+
? true
|
|
323
|
+
: value;
|
|
324
|
+
} else {
|
|
325
|
+
self.currentFilters[key] = "";
|
|
326
|
+
}
|
|
327
|
+
self.renderChartWithFilters();
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<option value="" ?selected=${self.currentFilters[key] == ""}>
|
|
331
|
+
All
|
|
332
|
+
</option>
|
|
333
|
+
${uniqueValues.map(
|
|
334
|
+
(val) => html`
|
|
335
|
+
<option
|
|
336
|
+
value=${val}
|
|
337
|
+
?selected=${self.currentFilters[key] == val.toString()}
|
|
338
|
+
>
|
|
339
|
+
${val}
|
|
340
|
+
</option>
|
|
341
|
+
`,
|
|
342
|
+
)}
|
|
343
|
+
</select>
|
|
344
|
+
</label>
|
|
345
|
+
</div>
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
render(
|
|
350
|
+
this.dropdownTarget,
|
|
351
|
+
html`<div class="benchmark-filter-dropdowns">${dropdowns}</div>`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
renderChartWithFilters() {
|
|
355
|
+
const queryFilter = { ...this.query, ...this.currentFilters };
|
|
356
|
+
let data = Utils.sortBenchmarksByParams(
|
|
357
|
+
Utils.getBenchmarkData(this.results, queryFilter),
|
|
358
|
+
this.sortBy,
|
|
359
|
+
);
|
|
360
|
+
let comparisonData = [];
|
|
361
|
+
let comparing = false;
|
|
362
|
+
if (this.currentComparisonData?.benchmarks) {
|
|
363
|
+
comparisonData = Utils.sortBenchmarksByParams(
|
|
364
|
+
Utils.getBenchmarkData(this.currentComparisonData, queryFilter),
|
|
365
|
+
this.sortBy,
|
|
366
|
+
);
|
|
367
|
+
comparisonData.forEach((benchmark) => {
|
|
368
|
+
benchmark.forComparison = true;
|
|
369
|
+
});
|
|
370
|
+
comparing = true;
|
|
371
|
+
data = comparisonData.concat(data);
|
|
372
|
+
}
|
|
373
|
+
let legendGroupKey =
|
|
374
|
+
queryFilter?.storage_type == ""
|
|
375
|
+
? "storage_type"
|
|
376
|
+
: Object.entries(queryFilter).find((entry) => entry[1] === "")?.[0];
|
|
377
|
+
|
|
378
|
+
function legendKeyFunc(benchmark, key) {
|
|
379
|
+
return key === "group" ? benchmark.group : benchmark.params[key];
|
|
380
|
+
}
|
|
381
|
+
let title = this.chartTitle(queryFilter);
|
|
382
|
+
if (comparing) {
|
|
383
|
+
title = `${title}<br><b style='color: var(--color-yellow)'>Compared with: ${this.currentCompare}</b>`;
|
|
384
|
+
}
|
|
385
|
+
Plotly.newPlot(
|
|
386
|
+
this.chartTarget,
|
|
387
|
+
data.map((benchmark) => {
|
|
388
|
+
let name = Utils.nameTransform(benchmark, true, queryFilter);
|
|
389
|
+
return {
|
|
390
|
+
type: "box",
|
|
391
|
+
name: name,
|
|
392
|
+
opacity: benchmark.forComparison ? 0.5 : comparing ? 0.75 : 1,
|
|
393
|
+
y: benchmark.stats.data || [
|
|
394
|
+
benchmark.stats.min * 1e3,
|
|
395
|
+
benchmark.stats.q1 * 1e3,
|
|
396
|
+
benchmark.stats.median * 1e3,
|
|
397
|
+
benchmark.stats.q3 * 1e3,
|
|
398
|
+
benchmark.stats.max * 1e3,
|
|
399
|
+
],
|
|
400
|
+
boxmean: true,
|
|
401
|
+
boxpoints: false,
|
|
402
|
+
line: { width: 1 },
|
|
403
|
+
marker: {
|
|
404
|
+
color: benchmark.forComparison
|
|
405
|
+
? Utils.color("yellow")
|
|
406
|
+
: Utils.getColorForStorage(benchmark.params.storage_type),
|
|
407
|
+
},
|
|
408
|
+
showlegend: !benchmark.forComparison,
|
|
409
|
+
legendgroup: legendKeyFunc(benchmark, legendGroupKey),
|
|
410
|
+
legendgrouptitle: {
|
|
411
|
+
text: Utils.formatParam(
|
|
412
|
+
legendGroupKey,
|
|
413
|
+
legendKeyFunc(benchmark, legendGroupKey),
|
|
414
|
+
),
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}),
|
|
418
|
+
{
|
|
419
|
+
yaxis: {
|
|
420
|
+
title: { text: "Time (ms)" },
|
|
421
|
+
exponentformat: "none",
|
|
422
|
+
ticksuffix: " ms",
|
|
423
|
+
tickformat: ",.2f",
|
|
424
|
+
type: "log",
|
|
425
|
+
autorange: true,
|
|
426
|
+
},
|
|
427
|
+
xaxis: { automargin: true },
|
|
428
|
+
title: {
|
|
429
|
+
text: title,
|
|
430
|
+
},
|
|
431
|
+
paper_bgcolor: Utils.color("background-secondary"),
|
|
432
|
+
plot_bgcolor: Utils.color("background-secondary"),
|
|
433
|
+
font: {
|
|
434
|
+
family: "Fira Sans",
|
|
435
|
+
color: Utils.color("fg"),
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
responsive: true,
|
|
440
|
+
displaylogo: false,
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
render() {
|
|
445
|
+
this.chart.innerHTML = "";
|
|
446
|
+
this.chart.querySelector(".benchmark-chart-loading")?.remove();
|
|
447
|
+
this.results = currentLoader.sources[this.source];
|
|
448
|
+
this.allBenchmarks = Utils.getBenchmarkData(this.results, this.query);
|
|
449
|
+
this.currentFilters = Object.fromEntries(
|
|
450
|
+
Object.entries(this.filters).map(([key, value]) => {
|
|
451
|
+
return typeof value.default === "boolean"
|
|
452
|
+
? [key, value.default]
|
|
453
|
+
: [key, value.default != null ? value.default.toString() : ""];
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
this.currentCompare = "";
|
|
457
|
+
this.currentComparisonData = null;
|
|
458
|
+
this.dropdownTarget = document.createElement("div");
|
|
459
|
+
this.compareTarget = document.createElement("div");
|
|
460
|
+
this.dropdownTarget.classList.add("benchmark-filters");
|
|
461
|
+
this.chartTarget = document.createElement("div");
|
|
462
|
+
this.chart.append(this.chartTarget);
|
|
463
|
+
this.chart.append(this.dropdownTarget);
|
|
464
|
+
this.chart.append(this.compareTarget);
|
|
465
|
+
this.renderDropdowns();
|
|
466
|
+
this.renderCompare();
|
|
467
|
+
this.renderChartWithFilters();
|
|
468
|
+
let initial = true;
|
|
469
|
+
this.chartTarget.on("plotly_afterplot", function () {
|
|
470
|
+
const { hash } = window.location;
|
|
471
|
+
if (hash && initial) {
|
|
472
|
+
initial = false;
|
|
473
|
+
const target = document.querySelector(hash);
|
|
474
|
+
if (target) {
|
|
475
|
+
setTimeout(function () {
|
|
476
|
+
target.scrollIntoView({ behavior: "instant" });
|
|
477
|
+
}, 10);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
485
|
+
const charts = document.querySelectorAll(".benchmark-chart");
|
|
486
|
+
charts.forEach((chart) => {
|
|
487
|
+
const chartObject = new BenchmarkChart(chart);
|
|
488
|
+
});
|
|
489
|
+
});
|