redis-benchmarks-specification 0.1.274__py3-none-any.whl → 0.1.275__py3-none-any.whl
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.
Potentially problematic release.
This version of redis-benchmarks-specification might be problematic. Click here for more details.
- redis_benchmarks_specification/__common__/timeseries.py +28 -6
- redis_benchmarks_specification/__runner__/args.py +36 -0
- redis_benchmarks_specification/__runner__/remote_profiling.py +329 -0
- redis_benchmarks_specification/__runner__/runner.py +496 -53
- redis_benchmarks_specification/test-suites/defaults.yml +3 -0
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-hash-hscan-50-fields-10B-values.yml +1 -1
- {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.275.dist-info}/METADATA +1 -1
- {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.275.dist-info}/RECORD +11 -16
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-hash-hscan-1K-fields-100B-values-cursor-count-1000.yml +0 -34
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-hash-hscan-1K-fields-10B-values-cursor-count-100.yml +0 -34
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-hash-hscan-1K-fields-10B-values.yml +0 -34
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-set-1K-elements-sscan-cursor-count-100.yml +0 -32
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-set-1K-elements-sscan.yml +0 -32
- redis_benchmarks_specification/test-suites/memtier_benchmark-1key-zset-1K-elements-zscan.yml +0 -32
- {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.275.dist-info}/LICENSE +0 -0
- {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.275.dist-info}/WHEEL +0 -0
- {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.275.dist-info}/entry_points.txt +0 -0
|
@@ -133,6 +133,9 @@ def extract_results_table(
|
|
|
133
133
|
use_metric_context_path = False
|
|
134
134
|
if len(find_res) > 1:
|
|
135
135
|
use_metric_context_path = True
|
|
136
|
+
# Always use context path for precision_summary metrics to show actual precision levels
|
|
137
|
+
if "precision_summary" in metric_jsonpath and "*" in metric_jsonpath:
|
|
138
|
+
use_metric_context_path = True
|
|
136
139
|
for metric in find_res:
|
|
137
140
|
metric_name = str(metric.path)
|
|
138
141
|
metric_value = float(metric.value)
|
|
@@ -142,15 +145,34 @@ def extract_results_table(
|
|
|
142
145
|
if metric_jsonpath[0] == ".":
|
|
143
146
|
metric_jsonpath = metric_jsonpath[1:]
|
|
144
147
|
|
|
148
|
+
# For precision_summary metrics, construct the full resolved path for display
|
|
149
|
+
display_path = metric_jsonpath
|
|
150
|
+
if "precision_summary" in metric_jsonpath and "*" in metric_jsonpath and use_metric_context_path:
|
|
151
|
+
# Replace the wildcard with the actual precision level
|
|
152
|
+
display_path = metric_jsonpath.replace("*", metric_context_path)
|
|
153
|
+
|
|
145
154
|
# retro-compatible naming
|
|
146
155
|
if use_metric_context_path is False:
|
|
147
156
|
metric_name = metric_jsonpath
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
else:
|
|
158
|
+
# For display purposes, use the resolved path for precision_summary
|
|
159
|
+
if "precision_summary" in metric_jsonpath and "*" in metric_jsonpath:
|
|
160
|
+
metric_name = display_path
|
|
161
|
+
else:
|
|
162
|
+
# Clean up the metric name for other cases
|
|
163
|
+
metric_name = metric_name.replace("'", "")
|
|
164
|
+
metric_name = metric_name.replace('"', "")
|
|
165
|
+
metric_name = metric_name.replace("(", "")
|
|
166
|
+
metric_name = metric_name.replace(")", "")
|
|
167
|
+
metric_name = metric_name.replace(" ", "_")
|
|
168
|
+
|
|
169
|
+
# Apply standard cleaning to all metric names
|
|
170
|
+
if not ("precision_summary" in metric_jsonpath and "*" in metric_jsonpath and use_metric_context_path):
|
|
171
|
+
metric_name = metric_name.replace("'", "")
|
|
172
|
+
metric_name = metric_name.replace('"', "")
|
|
173
|
+
metric_name = metric_name.replace("(", "")
|
|
174
|
+
metric_name = metric_name.replace(")", "")
|
|
175
|
+
metric_name = metric_name.replace(" ", "_")
|
|
154
176
|
|
|
155
177
|
results_matrix.append(
|
|
156
178
|
[
|
|
@@ -225,4 +225,40 @@ def create_client_runner_args(project_name):
|
|
|
225
225
|
default="",
|
|
226
226
|
help="UNIX Domain socket name",
|
|
227
227
|
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--enable-remote-profiling",
|
|
230
|
+
default=False,
|
|
231
|
+
action="store_true",
|
|
232
|
+
help="Enable remote profiling of Redis processes via HTTP GET endpoint. Profiles are collected in folded format during benchmark execution.",
|
|
233
|
+
)
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"--remote-profile-host",
|
|
236
|
+
type=str,
|
|
237
|
+
default="localhost",
|
|
238
|
+
help="Host for remote profiling HTTP endpoint. Default is localhost.",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--remote-profile-port",
|
|
242
|
+
type=int,
|
|
243
|
+
default=8080,
|
|
244
|
+
help="Port for remote profiling HTTP endpoint. Default is 8080.",
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"--remote-profile-output-dir",
|
|
248
|
+
type=str,
|
|
249
|
+
default="profiles",
|
|
250
|
+
help="Directory to store remote profiling output files. Default is 'profiles/'.",
|
|
251
|
+
)
|
|
252
|
+
parser.add_argument(
|
|
253
|
+
"--remote-profile-username",
|
|
254
|
+
type=str,
|
|
255
|
+
default=None,
|
|
256
|
+
help="Username for HTTP basic authentication to remote profiling endpoint. Optional.",
|
|
257
|
+
)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"--remote-profile-password",
|
|
260
|
+
type=str,
|
|
261
|
+
default=None,
|
|
262
|
+
help="Password for HTTP basic authentication to remote profiling endpoint. Optional.",
|
|
263
|
+
)
|
|
228
264
|
return parser
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remote profiling utilities for Redis benchmark runner.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to trigger remote profiling of Redis processes
|
|
5
|
+
via HTTP GET endpoints during benchmark execution. Profiles are collected in
|
|
6
|
+
folded format for performance analysis.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_redis_pid(redis_conn) -> Optional[int]:
|
|
20
|
+
"""
|
|
21
|
+
Extract Redis process ID from Redis INFO command.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
redis_conn: Redis connection object
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Redis process ID as integer, or None if not found
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
redis_info = redis_conn.info()
|
|
31
|
+
pid = redis_info.get("process_id")
|
|
32
|
+
if pid is not None:
|
|
33
|
+
logging.info(f"Extracted Redis PID: {pid}")
|
|
34
|
+
return int(pid)
|
|
35
|
+
else:
|
|
36
|
+
logging.warning("Redis process_id not found in INFO command")
|
|
37
|
+
return None
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logging.error(f"Failed to extract Redis PID: {e}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_redis_metadata(redis_conn) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Extract Redis metadata for profile comments.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
redis_conn: Redis connection object
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary containing Redis metadata
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
redis_info = redis_conn.info()
|
|
55
|
+
metadata = {
|
|
56
|
+
"redis_version": redis_info.get("redis_version", "unknown"),
|
|
57
|
+
"redis_git_sha1": redis_info.get("redis_git_sha1", "unknown"),
|
|
58
|
+
"redis_git_dirty": redis_info.get("redis_git_dirty", "unknown"),
|
|
59
|
+
"redis_build_id": redis_info.get("redis_build_id", "unknown"),
|
|
60
|
+
"process_id": redis_info.get("process_id", "unknown"),
|
|
61
|
+
"tcp_port": redis_info.get("tcp_port", "unknown"),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Use build_id if git_sha1 is empty or 0
|
|
65
|
+
if metadata["redis_git_sha1"] in ("", 0, "0"):
|
|
66
|
+
metadata["redis_git_sha1"] = metadata["redis_build_id"]
|
|
67
|
+
|
|
68
|
+
logging.info(f"Extracted Redis metadata: version={metadata['redis_version']}, sha={metadata['redis_git_sha1']}, pid={metadata['process_id']}")
|
|
69
|
+
return metadata
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logging.error(f"Failed to extract Redis metadata: {e}")
|
|
72
|
+
return {
|
|
73
|
+
"redis_version": "unknown",
|
|
74
|
+
"redis_git_sha1": "unknown",
|
|
75
|
+
"redis_git_dirty": "unknown",
|
|
76
|
+
"redis_build_id": "unknown",
|
|
77
|
+
"process_id": "unknown",
|
|
78
|
+
"tcp_port": "unknown",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def calculate_profile_duration(benchmark_duration_seconds: int) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Calculate profiling duration based on benchmark duration.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
benchmark_duration_seconds: Expected benchmark duration in seconds
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Profiling duration in seconds (minimum: benchmark duration, maximum: 30)
|
|
91
|
+
"""
|
|
92
|
+
# Minimum duration is the benchmark duration, maximum is 30 seconds
|
|
93
|
+
duration = min(max(benchmark_duration_seconds, 10), 30)
|
|
94
|
+
logging.info(f"Calculated profile duration: {duration}s (benchmark: {benchmark_duration_seconds}s)")
|
|
95
|
+
return duration
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def trigger_remote_profile(
|
|
99
|
+
host: str,
|
|
100
|
+
port: int,
|
|
101
|
+
pid: int,
|
|
102
|
+
duration: int,
|
|
103
|
+
timeout: int = 60,
|
|
104
|
+
username: Optional[str] = None,
|
|
105
|
+
password: Optional[str] = None
|
|
106
|
+
) -> Optional[str]:
|
|
107
|
+
"""
|
|
108
|
+
Trigger remote profiling via HTTP GET request.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
host: Remote host address
|
|
112
|
+
port: Remote port number
|
|
113
|
+
pid: Redis process ID
|
|
114
|
+
duration: Profiling duration in seconds
|
|
115
|
+
timeout: HTTP request timeout in seconds
|
|
116
|
+
username: Optional username for HTTP basic authentication
|
|
117
|
+
password: Optional password for HTTP basic authentication
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Profile content in folded format, or None if failed
|
|
121
|
+
"""
|
|
122
|
+
url = f"http://{host}:{port}/debug/folded/profile"
|
|
123
|
+
params = {
|
|
124
|
+
"pid": pid,
|
|
125
|
+
"seconds": duration
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Prepare authentication if provided
|
|
129
|
+
auth = None
|
|
130
|
+
if username is not None and password is not None:
|
|
131
|
+
auth = (username, password)
|
|
132
|
+
logging.info(f"Using HTTP basic authentication with username: {username}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
logging.info(f"Triggering remote profile: {url} with PID={pid}, duration={duration}s")
|
|
136
|
+
response = requests.get(url, params=params, timeout=timeout, auth=auth)
|
|
137
|
+
response.raise_for_status()
|
|
138
|
+
|
|
139
|
+
profile_content = response.text
|
|
140
|
+
logging.info(f"Successfully collected profile: {len(profile_content)} characters")
|
|
141
|
+
return profile_content
|
|
142
|
+
|
|
143
|
+
except requests.exceptions.Timeout:
|
|
144
|
+
logging.error(f"Remote profiling request timed out after {timeout}s")
|
|
145
|
+
return None
|
|
146
|
+
except requests.exceptions.ConnectionError:
|
|
147
|
+
logging.error(f"Failed to connect to remote profiling endpoint: {host}:{port}")
|
|
148
|
+
return None
|
|
149
|
+
except requests.exceptions.HTTPError as e:
|
|
150
|
+
logging.error(f"HTTP error during remote profiling: {e}")
|
|
151
|
+
if e.response.status_code == 401:
|
|
152
|
+
logging.error("Authentication failed - check username and password")
|
|
153
|
+
return None
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logging.error(f"Unexpected error during remote profiling: {e}")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def save_profile_with_metadata(
|
|
160
|
+
profile_content: str,
|
|
161
|
+
benchmark_name: str,
|
|
162
|
+
output_dir: str,
|
|
163
|
+
redis_metadata: Dict[str, Any],
|
|
164
|
+
duration: int
|
|
165
|
+
) -> Optional[str]:
|
|
166
|
+
"""
|
|
167
|
+
Save profile content to file with metadata comments.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
profile_content: Profile data in folded format
|
|
171
|
+
benchmark_name: Name of the benchmark
|
|
172
|
+
output_dir: Output directory path
|
|
173
|
+
redis_metadata: Redis metadata dictionary
|
|
174
|
+
duration: Profiling duration in seconds
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Path to saved file, or None if failed
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
# Create output directory if it doesn't exist
|
|
181
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
182
|
+
|
|
183
|
+
# Generate filename
|
|
184
|
+
filename = f"{benchmark_name}.folded"
|
|
185
|
+
filepath = os.path.join(output_dir, filename)
|
|
186
|
+
|
|
187
|
+
# Generate timestamp
|
|
188
|
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
189
|
+
|
|
190
|
+
# Create metadata comment
|
|
191
|
+
metadata_comment = (
|
|
192
|
+
f"# profile from redis sha = {redis_metadata['redis_git_sha1']} "
|
|
193
|
+
f"and pid {redis_metadata['process_id']} for duration of {duration}s. "
|
|
194
|
+
f"collection in date {timestamp}\n"
|
|
195
|
+
f"# redis_version: {redis_metadata['redis_version']}\n"
|
|
196
|
+
f"# redis_git_dirty: {redis_metadata['redis_git_dirty']}\n"
|
|
197
|
+
f"# tcp_port: {redis_metadata['tcp_port']}\n"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Write file with metadata and profile content
|
|
201
|
+
with open(filepath, 'w') as f:
|
|
202
|
+
f.write(metadata_comment)
|
|
203
|
+
f.write(profile_content)
|
|
204
|
+
|
|
205
|
+
logging.info(f"Saved profile to: {filepath}")
|
|
206
|
+
return filepath
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logging.error(f"Failed to save profile file: {e}")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RemoteProfiler:
|
|
214
|
+
"""
|
|
215
|
+
Remote profiler class to handle threaded profiling execution.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(self, host: str, port: int, output_dir: str, username: Optional[str] = None, password: Optional[str] = None):
|
|
219
|
+
self.host = host
|
|
220
|
+
self.port = port
|
|
221
|
+
self.output_dir = output_dir
|
|
222
|
+
self.username = username
|
|
223
|
+
self.password = password
|
|
224
|
+
self.profile_thread = None
|
|
225
|
+
self.profile_result = None
|
|
226
|
+
self.profile_error = None
|
|
227
|
+
|
|
228
|
+
def start_profiling(
|
|
229
|
+
self,
|
|
230
|
+
redis_conn,
|
|
231
|
+
benchmark_name: str,
|
|
232
|
+
benchmark_duration_seconds: int
|
|
233
|
+
) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Start profiling in a separate thread.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
redis_conn: Redis connection object
|
|
239
|
+
benchmark_name: Name of the benchmark
|
|
240
|
+
benchmark_duration_seconds: Expected benchmark duration
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if profiling thread started successfully, False otherwise
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
# Extract Redis metadata and PID
|
|
247
|
+
redis_metadata = extract_redis_metadata(redis_conn)
|
|
248
|
+
pid = redis_metadata.get("process_id")
|
|
249
|
+
|
|
250
|
+
if pid == "unknown" or pid is None:
|
|
251
|
+
logging.error("Cannot start remote profiling: Redis PID not available")
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
# Calculate profiling duration
|
|
255
|
+
duration = calculate_profile_duration(benchmark_duration_seconds)
|
|
256
|
+
|
|
257
|
+
# Start profiling thread
|
|
258
|
+
self.profile_thread = threading.Thread(
|
|
259
|
+
target=self._profile_worker,
|
|
260
|
+
args=(pid, duration, benchmark_name, redis_metadata),
|
|
261
|
+
daemon=True
|
|
262
|
+
)
|
|
263
|
+
self.profile_thread.start()
|
|
264
|
+
|
|
265
|
+
logging.info(f"Started remote profiling thread for benchmark: {benchmark_name}")
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logging.error(f"Failed to start remote profiling: {e}")
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
def _profile_worker(self, pid: int, duration: int, benchmark_name: str, redis_metadata: Dict[str, Any]):
|
|
273
|
+
"""
|
|
274
|
+
Worker function for profiling thread.
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
# Trigger remote profiling
|
|
278
|
+
profile_content = trigger_remote_profile(
|
|
279
|
+
self.host, self.port, pid, duration,
|
|
280
|
+
username=self.username, password=self.password
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if profile_content:
|
|
284
|
+
# Save profile with metadata
|
|
285
|
+
filepath = save_profile_with_metadata(
|
|
286
|
+
profile_content, benchmark_name, self.output_dir, redis_metadata, duration
|
|
287
|
+
)
|
|
288
|
+
self.profile_result = filepath
|
|
289
|
+
else:
|
|
290
|
+
self.profile_error = "Failed to collect profile content"
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.profile_error = f"Profile worker error: {e}"
|
|
294
|
+
logging.error(self.profile_error)
|
|
295
|
+
|
|
296
|
+
def wait_for_completion(self, timeout: int = 60) -> bool:
|
|
297
|
+
"""
|
|
298
|
+
Wait for profiling thread to complete.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
timeout: Maximum time to wait in seconds
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
True if completed successfully, False if timed out or failed
|
|
305
|
+
"""
|
|
306
|
+
if self.profile_thread is None:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
self.profile_thread.join(timeout=timeout)
|
|
311
|
+
|
|
312
|
+
if self.profile_thread.is_alive():
|
|
313
|
+
logging.warning(f"Remote profiling thread did not complete within {timeout}s")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
if self.profile_error:
|
|
317
|
+
logging.error(f"Remote profiling failed: {self.profile_error}")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
if self.profile_result:
|
|
321
|
+
logging.info(f"Remote profiling completed successfully: {self.profile_result}")
|
|
322
|
+
return True
|
|
323
|
+
else:
|
|
324
|
+
logging.warning("Remote profiling completed but no result available")
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logging.error(f"Error waiting for remote profiling completion: {e}")
|
|
329
|
+
return False
|