redis-benchmarks-specification 0.1.274__py3-none-any.whl → 0.1.276__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.

Files changed (32) hide show
  1. redis_benchmarks_specification/__common__/timeseries.py +28 -6
  2. redis_benchmarks_specification/__runner__/args.py +43 -1
  3. redis_benchmarks_specification/__runner__/remote_profiling.py +329 -0
  4. redis_benchmarks_specification/__runner__/runner.py +603 -67
  5. redis_benchmarks_specification/test-suites/defaults.yml +3 -0
  6. redis_benchmarks_specification/test-suites/memtier_benchmark-10Mkeys-string-get-10B-pipeline-100-nokeyprefix.yml +4 -0
  7. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-100B-expire-use-case.yml +2 -2
  8. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-10B-expire-use-case.yml +2 -2
  9. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-10B-psetex-expire-use-case.yml +2 -2
  10. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-10B-setex-expire-use-case.yml +2 -2
  11. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-1KiB-expire-use-case.yml +2 -2
  12. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-4KiB-expire-use-case.yml +2 -2
  13. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-string-get-10B-pipeline-100-nokeyprefix.yml +4 -0
  14. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-string-mixed-50-50-set-get-with-expiration-240B-400_conns.yml +4 -2
  15. redis_benchmarks_specification/test-suites/memtier_benchmark-1Mkeys-string-set-with-ex-100B-pipeline-10.yml +1 -1
  16. redis_benchmarks_specification/test-suites/memtier_benchmark-1key-set-1K-elements-sscan-cursor-count-100.yml +1 -1
  17. redis_benchmarks_specification/test-suites/memtier_benchmark-1key-set-1K-elements-sscan.yml +1 -1
  18. redis_benchmarks_specification/test-suites/memtier_benchmark-1key-zset-1K-elements-zscan.yml +1 -1
  19. redis_benchmarks_specification/test-suites/memtier_benchmark-3Mkeys-string-mixed-50-50-with-512B-values-with-expiration-pipeline-10-400_conns.yml +0 -2
  20. {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.276.dist-info}/METADATA +1 -1
  21. {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.276.dist-info}/RECORD +24 -31
  22. redis_benchmarks_specification/test-suites/memtier_benchmark-10Mkeys-string-set-update-del-ex-36000-pipeline-10.yml +0 -32
  23. redis_benchmarks_specification/test-suites/memtier_benchmark-150Mkeys-string-set-ex-20-pipeline-10.yml +0 -30
  24. redis_benchmarks_specification/test-suites/memtier_benchmark-50Mkeys-string-set-ex-10-with-precondition-pipeline-10.yml +0 -34
  25. redis_benchmarks_specification/test-suites/memtier_benchmark-50Mkeys-string-set-ex-10years-pipeline-10.yml +0 -30
  26. redis_benchmarks_specification/test-suites/memtier_benchmark-50Mkeys-string-set-ex-3-pipeline-10.yml +0 -30
  27. redis_benchmarks_specification/test-suites/memtier_benchmark-50Mkeys-string-set-ex-random-range-pipeline-10.yml +0 -30
  28. redis_benchmarks_specification/test-suites/memtier_benchmark-50Mkeys-string-set-update-del-ex-120-pipeline-10.yml +0 -32
  29. redis_benchmarks_specification/test-suites/memtier_benchmark-80Mkeys-string-set-ex-20-precodition-multiclient-pipeline-10.yml +0 -34
  30. {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.276.dist-info}/LICENSE +0 -0
  31. {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.276.dist-info}/WHEEL +0 -0
  32. {redis_benchmarks_specification-0.1.274.dist-info → redis_benchmarks_specification-0.1.276.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
- metric_name = metric_name.replace("'", "")
150
- metric_name = metric_name.replace('"', "")
151
- metric_name = metric_name.replace("(", "")
152
- metric_name = metric_name.replace(")", "")
153
- metric_name = metric_name.replace(" ", "_")
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
  [
@@ -208,11 +208,17 @@ def create_client_runner_args(project_name):
208
208
  type=int,
209
209
  help="override memtier number of runs for each benchmark. By default will run once each test",
210
210
  )
211
+ parser.add_argument(
212
+ "--timeout-buffer",
213
+ default=60,
214
+ type=int,
215
+ help="Buffer time in seconds to add to test-time for process timeout (both Docker containers and local processes). Default is 60 seconds.",
216
+ )
211
217
  parser.add_argument(
212
218
  "--container-timeout-buffer",
213
219
  default=60,
214
220
  type=int,
215
- help="Buffer time in seconds to add to test-time for container timeout. Default is 60 seconds.",
221
+ help="Deprecated: Use --timeout-buffer instead. Buffer time in seconds to add to test-time for container timeout.",
216
222
  )
217
223
  parser.add_argument(
218
224
  "--cluster-mode",
@@ -225,4 +231,40 @@ def create_client_runner_args(project_name):
225
231
  default="",
226
232
  help="UNIX Domain socket name",
227
233
  )
234
+ parser.add_argument(
235
+ "--enable-remote-profiling",
236
+ default=False,
237
+ action="store_true",
238
+ help="Enable remote profiling of Redis processes via HTTP GET endpoint. Profiles are collected in folded format during benchmark execution.",
239
+ )
240
+ parser.add_argument(
241
+ "--remote-profile-host",
242
+ type=str,
243
+ default="localhost",
244
+ help="Host for remote profiling HTTP endpoint. Default is localhost.",
245
+ )
246
+ parser.add_argument(
247
+ "--remote-profile-port",
248
+ type=int,
249
+ default=8080,
250
+ help="Port for remote profiling HTTP endpoint. Default is 8080.",
251
+ )
252
+ parser.add_argument(
253
+ "--remote-profile-output-dir",
254
+ type=str,
255
+ default="profiles",
256
+ help="Directory to store remote profiling output files. Default is 'profiles/'.",
257
+ )
258
+ parser.add_argument(
259
+ "--remote-profile-username",
260
+ type=str,
261
+ default=None,
262
+ help="Username for HTTP basic authentication to remote profiling endpoint. Optional.",
263
+ )
264
+ parser.add_argument(
265
+ "--remote-profile-password",
266
+ type=str,
267
+ default=None,
268
+ help="Password for HTTP basic authentication to remote profiling endpoint. Optional.",
269
+ )
228
270
  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