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.
Files changed (102) hide show
  1. {limits-5.0.0rc1 → limits-5.1.0}/HISTORY.rst +33 -6
  2. {limits-5.0.0rc1 → limits-5.1.0}/PKG-INFO +3 -2
  3. {limits-5.0.0rc1 → limits-5.1.0}/README.rst +1 -0
  4. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/conf.py +12 -1
  5. limits-5.1.0/doc/source/ext/_static/benchmark-chart.css +94 -0
  6. limits-5.1.0/doc/source/ext/_static/js/benchmark-chart.js +489 -0
  7. limits-5.1.0/doc/source/ext/_static/js/benchmark-details.js +117 -0
  8. limits-5.1.0/doc/source/ext/_static/js/benchmark-loader.js +43 -0
  9. limits-5.1.0/doc/source/ext/_templates/git_info.js +22 -0
  10. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/ext/bench_chart.py +15 -9
  11. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/installation.rst +6 -1
  12. limits-5.1.0/doc/source/performance.rst +219 -0
  13. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/theme_config.py +10 -0
  14. {limits-5.0.0rc1 → limits-5.1.0}/limits/_version.py +3 -3
  15. limits-5.0.0rc1/limits/aio/storage/memcached.py → limits-5.1.0/limits/aio/storage/memcached/__init__.py +43 -113
  16. limits-5.1.0/limits/aio/storage/memcached/bridge.py +73 -0
  17. limits-5.1.0/limits/aio/storage/memcached/emcache.py +112 -0
  18. limits-5.1.0/limits/aio/storage/memcached/memcachio.py +104 -0
  19. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/memory.py +29 -18
  20. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/__init__.py +22 -6
  21. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/bridge.py +3 -2
  22. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/memcached.py +2 -0
  23. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/memory.py +5 -5
  24. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis.py +5 -2
  25. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis_cluster.py +5 -2
  26. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/redis_sentinel.py +3 -0
  27. {limits-5.0.0rc1 → limits-5.1.0}/limits/typing.py +1 -0
  28. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/PKG-INFO +3 -2
  29. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/SOURCES.txt +4 -1
  30. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/requires.txt +1 -1
  31. {limits-5.0.0rc1 → limits-5.1.0}/requirements/docs.txt +3 -2
  32. {limits-5.0.0rc1 → limits-5.1.0}/requirements/main.txt +1 -1
  33. {limits-5.0.0rc1 → limits-5.1.0}/requirements/test.txt +1 -1
  34. {limits-5.0.0rc1 → limits-5.1.0}/setup.cfg +3 -0
  35. limits-5.0.0rc1/doc/source/ext/_static/benchmark-chart.css +0 -61
  36. limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-chart.js +0 -219
  37. limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-details.js +0 -103
  38. limits-5.0.0rc1/doc/source/ext/_static/js/benchmark-loader.js +0 -31
  39. limits-5.0.0rc1/doc/source/ext/_templates/git_info.js +0 -2
  40. limits-5.0.0rc1/doc/source/performance.rst +0 -221
  41. {limits-5.0.0rc1 → limits-5.1.0}/CLASSIFIERS +0 -0
  42. {limits-5.0.0rc1 → limits-5.1.0}/CONTRIBUTIONS.rst +0 -0
  43. {limits-5.0.0rc1 → limits-5.1.0}/LICENSE.txt +0 -0
  44. {limits-5.0.0rc1 → limits-5.1.0}/MANIFEST.in +0 -0
  45. {limits-5.0.0rc1 → limits-5.1.0}/doc/Makefile +0 -0
  46. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/_static/custom.css +0 -0
  47. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/api.rst +0 -0
  48. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/async.rst +0 -0
  49. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/changelog.rst +0 -0
  50. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/custom-storage.rst +0 -0
  51. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/index.rst +0 -0
  52. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/quickstart.rst +0 -0
  53. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/storage.rst +0 -0
  54. {limits-5.0.0rc1 → limits-5.1.0}/doc/source/strategies.rst +0 -0
  55. {limits-5.0.0rc1 → limits-5.1.0}/limits/__init__.py +0 -0
  56. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/__init__.py +0 -0
  57. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/__init__.py +0 -0
  58. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/base.py +0 -0
  59. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/mongodb.py +0 -0
  60. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/coredis.py +0 -0
  61. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/redispy.py +0 -0
  62. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/storage/redis/valkey.py +0 -0
  63. {limits-5.0.0rc1 → limits-5.1.0}/limits/aio/strategies.py +0 -0
  64. {limits-5.0.0rc1 → limits-5.1.0}/limits/errors.py +0 -0
  65. {limits-5.0.0rc1 → limits-5.1.0}/limits/limits.py +0 -0
  66. {limits-5.0.0rc1 → limits-5.1.0}/limits/py.typed +0 -0
  67. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  68. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
  69. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  70. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  71. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  72. {limits-5.0.0rc1 → limits-5.1.0}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
  73. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/__init__.py +0 -0
  74. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/base.py +0 -0
  75. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/mongodb.py +0 -0
  76. {limits-5.0.0rc1 → limits-5.1.0}/limits/storage/registry.py +0 -0
  77. {limits-5.0.0rc1 → limits-5.1.0}/limits/strategies.py +0 -0
  78. {limits-5.0.0rc1 → limits-5.1.0}/limits/util.py +0 -0
  79. {limits-5.0.0rc1 → limits-5.1.0}/limits/version.py +0 -0
  80. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/dependency_links.txt +0 -0
  81. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/not-zip-safe +0 -0
  82. {limits-5.0.0rc1 → limits-5.1.0}/limits.egg-info/top_level.txt +0 -0
  83. {limits-5.0.0rc1 → limits-5.1.0}/pyproject.toml +0 -0
  84. {limits-5.0.0rc1 → limits-5.1.0}/requirements/ci.txt +0 -0
  85. {limits-5.0.0rc1 → limits-5.1.0}/requirements/dev.txt +0 -0
  86. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-memcached.txt +0 -0
  87. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-mongodb.txt +0 -0
  88. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-redis.txt +0 -0
  89. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/async-valkey.txt +0 -0
  90. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/memcached.txt +0 -0
  91. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/mongodb.txt +0 -0
  92. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/redis.txt +0 -0
  93. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/rediscluster.txt +0 -0
  94. {limits-5.0.0rc1 → limits-5.1.0}/requirements/storage/valkey.txt +0 -0
  95. {limits-5.0.0rc1 → limits-5.1.0}/setup.py +0 -0
  96. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_limit_granularities.py +0 -0
  97. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_limits.py +0 -0
  98. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_ratelimit_parser.py +0 -0
  99. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_storage.py +0 -0
  100. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_strategy.py +0 -0
  101. {limits-5.0.0rc1 → limits-5.1.0}/tests/test_utils.py +0 -0
  102. {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.0rc1
7
- ---------
8
- Release Date: 2025-04-09
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
- * Replaced async support for memached from :pypi:`emcache` to :pypi`:memcachio`
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 in-memory moving window ``test`` and ``get_window_stats`` operations.
19
- * Improved performance of redis moving window ``test`` and ``get_window_stats`` operations.
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.0rc1
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,>=21
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
 
@@ -207,5 +207,6 @@ Links
207
207
  =====
208
208
 
209
209
  * `Documentation <http://limits.readthedocs.org/en/latest>`_
210
+ * `Benchmarks <http://limits.readthedocs.org/en/latest/performance.html>`_
210
211
  * `Changelog <http://limits.readthedocs.org/en/stable/changelog.html>`_
211
212
 
@@ -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
+ });