redis-benchmarks-specification 0.1.280__py3-none-any.whl → 0.1.282__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 +14 -3
- redis_benchmarks_specification/__compare__/args.py +24 -0
- redis_benchmarks_specification/__compare__/compare.py +161 -59
- redis_benchmarks_specification/__runner__/args.py +9 -3
- redis_benchmarks_specification/__runner__/remote_profiling.py +118 -81
- redis_benchmarks_specification/__runner__/runner.py +190 -76
- {redis_benchmarks_specification-0.1.280.dist-info → redis_benchmarks_specification-0.1.282.dist-info}/METADATA +1 -1
- {redis_benchmarks_specification-0.1.280.dist-info → redis_benchmarks_specification-0.1.282.dist-info}/RECORD +11 -11
- {redis_benchmarks_specification-0.1.280.dist-info → redis_benchmarks_specification-0.1.282.dist-info}/LICENSE +0 -0
- {redis_benchmarks_specification-0.1.280.dist-info → redis_benchmarks_specification-0.1.282.dist-info}/WHEEL +0 -0
- {redis_benchmarks_specification-0.1.280.dist-info → redis_benchmarks_specification-0.1.282.dist-info}/entry_points.txt +0 -0
|
@@ -3,7 +3,7 @@ Remote profiling utilities for Redis benchmark runner.
|
|
|
3
3
|
|
|
4
4
|
This module provides functionality to trigger remote profiling of Redis processes
|
|
5
5
|
via HTTP GET endpoints during benchmark execution. Profiles are collected in
|
|
6
|
-
|
|
6
|
+
pprof binary format for performance analysis.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
@@ -19,10 +19,10 @@ import requests
|
|
|
19
19
|
def extract_redis_pid(redis_conn) -> Optional[int]:
|
|
20
20
|
"""
|
|
21
21
|
Extract Redis process ID from Redis INFO command.
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
Args:
|
|
24
24
|
redis_conn: Redis connection object
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
Returns:
|
|
27
27
|
Redis process ID as integer, or None if not found
|
|
28
28
|
"""
|
|
@@ -43,10 +43,10 @@ def extract_redis_pid(redis_conn) -> Optional[int]:
|
|
|
43
43
|
def extract_redis_metadata(redis_conn) -> Dict[str, Any]:
|
|
44
44
|
"""
|
|
45
45
|
Extract Redis metadata for profile comments.
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
Args:
|
|
48
48
|
redis_conn: Redis connection object
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
Returns:
|
|
51
51
|
Dictionary containing Redis metadata
|
|
52
52
|
"""
|
|
@@ -60,18 +60,20 @@ def extract_redis_metadata(redis_conn) -> Dict[str, Any]:
|
|
|
60
60
|
"process_id": redis_info.get("process_id", "unknown"),
|
|
61
61
|
"tcp_port": redis_info.get("tcp_port", "unknown"),
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
# Use build_id if git_sha1 is empty or 0
|
|
65
65
|
if metadata["redis_git_sha1"] in ("", 0, "0"):
|
|
66
66
|
metadata["redis_git_sha1"] = metadata["redis_build_id"]
|
|
67
|
-
|
|
68
|
-
logging.info(
|
|
67
|
+
|
|
68
|
+
logging.info(
|
|
69
|
+
f"Extracted Redis metadata: version={metadata['redis_version']}, sha={metadata['redis_git_sha1']}, pid={metadata['process_id']}"
|
|
70
|
+
)
|
|
69
71
|
return metadata
|
|
70
72
|
except Exception as e:
|
|
71
73
|
logging.error(f"Failed to extract Redis metadata: {e}")
|
|
72
74
|
return {
|
|
73
75
|
"redis_version": "unknown",
|
|
74
|
-
"redis_git_sha1": "unknown",
|
|
76
|
+
"redis_git_sha1": "unknown",
|
|
75
77
|
"redis_git_dirty": "unknown",
|
|
76
78
|
"redis_build_id": "unknown",
|
|
77
79
|
"process_id": "unknown",
|
|
@@ -82,16 +84,18 @@ def extract_redis_metadata(redis_conn) -> Dict[str, Any]:
|
|
|
82
84
|
def calculate_profile_duration(benchmark_duration_seconds: int) -> int:
|
|
83
85
|
"""
|
|
84
86
|
Calculate profiling duration based on benchmark duration.
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
Args:
|
|
87
89
|
benchmark_duration_seconds: Expected benchmark duration in seconds
|
|
88
|
-
|
|
90
|
+
|
|
89
91
|
Returns:
|
|
90
92
|
Profiling duration in seconds (minimum: benchmark duration, maximum: 30)
|
|
91
93
|
"""
|
|
92
94
|
# Minimum duration is the benchmark duration, maximum is 30 seconds
|
|
93
95
|
duration = min(max(benchmark_duration_seconds, 10), 30)
|
|
94
|
-
logging.info(
|
|
96
|
+
logging.info(
|
|
97
|
+
f"Calculated profile duration: {duration}s (benchmark: {benchmark_duration_seconds}s)"
|
|
98
|
+
)
|
|
95
99
|
return duration
|
|
96
100
|
|
|
97
101
|
|
|
@@ -102,10 +106,10 @@ def trigger_remote_profile(
|
|
|
102
106
|
duration: int,
|
|
103
107
|
timeout: int = 60,
|
|
104
108
|
username: Optional[str] = None,
|
|
105
|
-
password: Optional[str] = None
|
|
106
|
-
) -> Optional[
|
|
109
|
+
password: Optional[str] = None,
|
|
110
|
+
) -> Optional[bytes]:
|
|
107
111
|
"""
|
|
108
|
-
Trigger remote profiling via HTTP GET request.
|
|
112
|
+
Trigger remote profiling via HTTP GET request using pprof endpoint.
|
|
109
113
|
|
|
110
114
|
Args:
|
|
111
115
|
host: Remote host address
|
|
@@ -117,13 +121,10 @@ def trigger_remote_profile(
|
|
|
117
121
|
password: Optional password for HTTP basic authentication
|
|
118
122
|
|
|
119
123
|
Returns:
|
|
120
|
-
Profile content in
|
|
124
|
+
Profile content in pprof binary format, or None if failed
|
|
121
125
|
"""
|
|
122
|
-
url = f"http://{host}:{port}/debug/
|
|
123
|
-
params = {
|
|
124
|
-
"pid": pid,
|
|
125
|
-
"seconds": duration
|
|
126
|
-
}
|
|
126
|
+
url = f"http://{host}:{port}/debug/pprof/profile"
|
|
127
|
+
params = {"pid": pid, "seconds": duration}
|
|
127
128
|
|
|
128
129
|
# Prepare authentication if provided
|
|
129
130
|
auth = None
|
|
@@ -132,12 +133,14 @@ def trigger_remote_profile(
|
|
|
132
133
|
logging.info(f"Using HTTP basic authentication with username: {username}")
|
|
133
134
|
|
|
134
135
|
try:
|
|
135
|
-
logging.info(
|
|
136
|
+
logging.info(
|
|
137
|
+
f"Triggering remote profile: {url} with PID={pid}, duration={duration}s"
|
|
138
|
+
)
|
|
136
139
|
response = requests.get(url, params=params, timeout=timeout, auth=auth)
|
|
137
140
|
response.raise_for_status()
|
|
138
141
|
|
|
139
|
-
profile_content = response.
|
|
140
|
-
logging.info(f"Successfully collected profile: {len(profile_content)}
|
|
142
|
+
profile_content = response.content
|
|
143
|
+
logging.info(f"Successfully collected profile: {len(profile_content)} bytes")
|
|
141
144
|
return profile_content
|
|
142
145
|
|
|
143
146
|
except requests.exceptions.Timeout:
|
|
@@ -157,55 +160,65 @@ def trigger_remote_profile(
|
|
|
157
160
|
|
|
158
161
|
|
|
159
162
|
def save_profile_with_metadata(
|
|
160
|
-
profile_content:
|
|
163
|
+
profile_content: bytes,
|
|
161
164
|
benchmark_name: str,
|
|
162
165
|
output_dir: str,
|
|
163
166
|
redis_metadata: Dict[str, Any],
|
|
164
|
-
duration: int
|
|
167
|
+
duration: int,
|
|
165
168
|
) -> Optional[str]:
|
|
166
169
|
"""
|
|
167
|
-
Save profile content to file
|
|
168
|
-
|
|
170
|
+
Save profile content to file in pprof binary format.
|
|
171
|
+
|
|
169
172
|
Args:
|
|
170
|
-
profile_content: Profile data in
|
|
173
|
+
profile_content: Profile data in pprof binary format
|
|
171
174
|
benchmark_name: Name of the benchmark
|
|
172
175
|
output_dir: Output directory path
|
|
173
176
|
redis_metadata: Redis metadata dictionary
|
|
174
177
|
duration: Profiling duration in seconds
|
|
175
|
-
|
|
178
|
+
|
|
176
179
|
Returns:
|
|
177
180
|
Path to saved file, or None if failed
|
|
178
181
|
"""
|
|
179
182
|
try:
|
|
180
183
|
# Create output directory if it doesn't exist
|
|
181
184
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
182
|
-
|
|
183
|
-
# Generate filename
|
|
184
|
-
filename = f"{benchmark_name}.
|
|
185
|
+
|
|
186
|
+
# Generate filename with .pb.gz extension
|
|
187
|
+
filename = f"{benchmark_name}.pb.gz"
|
|
185
188
|
filepath = os.path.join(output_dir, filename)
|
|
186
|
-
|
|
189
|
+
|
|
187
190
|
# Generate timestamp
|
|
188
191
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
189
|
-
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
f
|
|
192
|
+
|
|
193
|
+
# Write binary profile content directly
|
|
194
|
+
with open(filepath, "wb") as f:
|
|
195
|
+
f.write(profile_content)
|
|
196
|
+
|
|
197
|
+
# Create a separate metadata file
|
|
198
|
+
metadata_filename = f"{benchmark_name}.metadata.txt"
|
|
199
|
+
metadata_filepath = os.path.join(output_dir, metadata_filename)
|
|
200
|
+
|
|
201
|
+
metadata_content = (
|
|
202
|
+
f"Profile from redis sha = {redis_metadata['redis_git_sha1']} "
|
|
193
203
|
f"and pid {redis_metadata['process_id']} for duration of {duration}s. "
|
|
194
|
-
f"
|
|
195
|
-
f"
|
|
196
|
-
f"
|
|
197
|
-
f"
|
|
198
|
-
f"
|
|
204
|
+
f"Collection date: {timestamp}\n"
|
|
205
|
+
f"benchmark_name={benchmark_name}\n"
|
|
206
|
+
f"redis_git_sha1={redis_metadata['redis_git_sha1']}\n"
|
|
207
|
+
f"redis_version={redis_metadata['redis_version']}\n"
|
|
208
|
+
f"redis_git_dirty={redis_metadata['redis_git_dirty']}\n"
|
|
209
|
+
f"redis_build_id={redis_metadata['redis_build_id']}\n"
|
|
210
|
+
f"process_id={redis_metadata['process_id']}\n"
|
|
211
|
+
f"tcp_port={redis_metadata['tcp_port']}\n"
|
|
212
|
+
f"duration_seconds={duration}\n"
|
|
199
213
|
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
f.write(profile_content)
|
|
205
|
-
|
|
214
|
+
|
|
215
|
+
with open(metadata_filepath, "w") as f:
|
|
216
|
+
f.write(metadata_content)
|
|
217
|
+
|
|
206
218
|
logging.info(f"Saved profile to: {filepath}")
|
|
219
|
+
logging.info(f"Saved metadata to: {metadata_filepath}")
|
|
207
220
|
return filepath
|
|
208
|
-
|
|
221
|
+
|
|
209
222
|
except Exception as e:
|
|
210
223
|
logging.error(f"Failed to save profile file: {e}")
|
|
211
224
|
return None
|
|
@@ -216,7 +229,14 @@ class RemoteProfiler:
|
|
|
216
229
|
Remote profiler class to handle threaded profiling execution.
|
|
217
230
|
"""
|
|
218
231
|
|
|
219
|
-
def __init__(
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
host: str,
|
|
235
|
+
port: int,
|
|
236
|
+
output_dir: str,
|
|
237
|
+
username: Optional[str] = None,
|
|
238
|
+
password: Optional[str] = None,
|
|
239
|
+
):
|
|
220
240
|
self.host = host
|
|
221
241
|
self.port = port
|
|
222
242
|
self.output_dir = output_dir
|
|
@@ -225,21 +245,18 @@ class RemoteProfiler:
|
|
|
225
245
|
self.profile_thread = None
|
|
226
246
|
self.profile_result = None
|
|
227
247
|
self.profile_error = None
|
|
228
|
-
|
|
248
|
+
|
|
229
249
|
def start_profiling(
|
|
230
|
-
self,
|
|
231
|
-
redis_conn,
|
|
232
|
-
benchmark_name: str,
|
|
233
|
-
benchmark_duration_seconds: int
|
|
250
|
+
self, redis_conn, benchmark_name: str, benchmark_duration_seconds: int
|
|
234
251
|
) -> bool:
|
|
235
252
|
"""
|
|
236
253
|
Start profiling in a separate thread.
|
|
237
|
-
|
|
254
|
+
|
|
238
255
|
Args:
|
|
239
256
|
redis_conn: Redis connection object
|
|
240
257
|
benchmark_name: Name of the benchmark
|
|
241
258
|
benchmark_duration_seconds: Expected benchmark duration
|
|
242
|
-
|
|
259
|
+
|
|
243
260
|
Returns:
|
|
244
261
|
True if profiling thread started successfully, False otherwise
|
|
245
262
|
"""
|
|
@@ -247,44 +264,60 @@ class RemoteProfiler:
|
|
|
247
264
|
# Extract Redis metadata and PID
|
|
248
265
|
redis_metadata = extract_redis_metadata(redis_conn)
|
|
249
266
|
pid = redis_metadata.get("process_id")
|
|
250
|
-
|
|
267
|
+
|
|
251
268
|
if pid == "unknown" or pid is None:
|
|
252
269
|
logging.error("Cannot start remote profiling: Redis PID not available")
|
|
253
270
|
return False
|
|
254
|
-
|
|
271
|
+
|
|
255
272
|
# Calculate profiling duration
|
|
256
273
|
duration = calculate_profile_duration(benchmark_duration_seconds)
|
|
257
|
-
|
|
274
|
+
|
|
258
275
|
# Start profiling thread
|
|
259
276
|
self.profile_thread = threading.Thread(
|
|
260
277
|
target=self._profile_worker,
|
|
261
278
|
args=(pid, duration, benchmark_name, redis_metadata),
|
|
262
|
-
daemon=True
|
|
279
|
+
daemon=True,
|
|
263
280
|
)
|
|
264
281
|
self.profile_thread.start()
|
|
265
|
-
|
|
266
|
-
logging.info(
|
|
282
|
+
|
|
283
|
+
logging.info(
|
|
284
|
+
f"Started remote profiling thread for benchmark: {benchmark_name}"
|
|
285
|
+
)
|
|
267
286
|
return True
|
|
268
|
-
|
|
287
|
+
|
|
269
288
|
except Exception as e:
|
|
270
289
|
logging.error(f"Failed to start remote profiling: {e}")
|
|
271
290
|
return False
|
|
272
|
-
|
|
273
|
-
def _profile_worker(
|
|
291
|
+
|
|
292
|
+
def _profile_worker(
|
|
293
|
+
self,
|
|
294
|
+
pid: int,
|
|
295
|
+
duration: int,
|
|
296
|
+
benchmark_name: str,
|
|
297
|
+
redis_metadata: Dict[str, Any],
|
|
298
|
+
):
|
|
274
299
|
"""
|
|
275
300
|
Worker function for profiling thread.
|
|
276
301
|
"""
|
|
277
302
|
try:
|
|
278
303
|
# Trigger remote profiling
|
|
279
304
|
profile_content = trigger_remote_profile(
|
|
280
|
-
self.host,
|
|
281
|
-
|
|
305
|
+
self.host,
|
|
306
|
+
self.port,
|
|
307
|
+
pid,
|
|
308
|
+
duration,
|
|
309
|
+
username=self.username,
|
|
310
|
+
password=self.password,
|
|
282
311
|
)
|
|
283
312
|
|
|
284
|
-
if profile_content:
|
|
313
|
+
if profile_content is not None:
|
|
285
314
|
# Save profile with metadata
|
|
286
315
|
filepath = save_profile_with_metadata(
|
|
287
|
-
profile_content,
|
|
316
|
+
profile_content,
|
|
317
|
+
benchmark_name,
|
|
318
|
+
self.output_dir,
|
|
319
|
+
redis_metadata,
|
|
320
|
+
duration,
|
|
288
321
|
)
|
|
289
322
|
self.profile_result = filepath
|
|
290
323
|
else:
|
|
@@ -293,38 +326,42 @@ class RemoteProfiler:
|
|
|
293
326
|
except Exception as e:
|
|
294
327
|
self.profile_error = f"Profile worker error: {e}"
|
|
295
328
|
logging.error(self.profile_error)
|
|
296
|
-
|
|
329
|
+
|
|
297
330
|
def wait_for_completion(self, timeout: int = 60) -> bool:
|
|
298
331
|
"""
|
|
299
332
|
Wait for profiling thread to complete.
|
|
300
|
-
|
|
333
|
+
|
|
301
334
|
Args:
|
|
302
335
|
timeout: Maximum time to wait in seconds
|
|
303
|
-
|
|
336
|
+
|
|
304
337
|
Returns:
|
|
305
338
|
True if completed successfully, False if timed out or failed
|
|
306
339
|
"""
|
|
307
340
|
if self.profile_thread is None:
|
|
308
341
|
return False
|
|
309
|
-
|
|
342
|
+
|
|
310
343
|
try:
|
|
311
344
|
self.profile_thread.join(timeout=timeout)
|
|
312
|
-
|
|
345
|
+
|
|
313
346
|
if self.profile_thread.is_alive():
|
|
314
|
-
logging.warning(
|
|
347
|
+
logging.warning(
|
|
348
|
+
f"Remote profiling thread did not complete within {timeout}s"
|
|
349
|
+
)
|
|
315
350
|
return False
|
|
316
|
-
|
|
351
|
+
|
|
317
352
|
if self.profile_error:
|
|
318
353
|
logging.error(f"Remote profiling failed: {self.profile_error}")
|
|
319
354
|
return False
|
|
320
|
-
|
|
355
|
+
|
|
321
356
|
if self.profile_result:
|
|
322
|
-
logging.info(
|
|
357
|
+
logging.info(
|
|
358
|
+
f"Remote profiling completed successfully: {self.profile_result}"
|
|
359
|
+
)
|
|
323
360
|
return True
|
|
324
361
|
else:
|
|
325
362
|
logging.warning("Remote profiling completed but no result available")
|
|
326
363
|
return False
|
|
327
|
-
|
|
364
|
+
|
|
328
365
|
except Exception as e:
|
|
329
366
|
logging.error(f"Error waiting for remote profiling completion: {e}")
|
|
330
367
|
return False
|