rsloop 0.1.8__tar.gz → 0.1.9__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.
- {rsloop-0.1.8 → rsloop-0.1.9}/Cargo.lock +1 -1
- {rsloop-0.1.8 → rsloop-0.1.9}/Cargo.toml +1 -1
- {rsloop-0.1.8 → rsloop-0.1.9}/PKG-INFO +2 -6
- {rsloop-0.1.8 → rsloop-0.1.9}/README.md +1 -5
- {rsloop-0.1.8 → rsloop-0.1.9}/benchmarks/README.md +5 -4
- {rsloop-0.1.8 → rsloop-0.1.9}/benchmarks/compare_event_loops.py +102 -40
- {rsloop-0.1.8 → rsloop-0.1.9}/justfile +4 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/pyproject.toml +1 -1
- {rsloop-0.1.8 → rsloop-0.1.9}/python/rsloop/__init__.py +11 -19
- {rsloop-0.1.8 → rsloop-0.1.9}/src/callbacks.rs +8 -11
- {rsloop-0.1.8 → rsloop-0.1.9}/src/profiler.rs +9 -29
- {rsloop-0.1.8 → rsloop-0.1.9}/src/stream_transport.rs +15 -12
- {rsloop-0.1.8 → rsloop-0.1.9}/uv.lock +1 -1
- {rsloop-0.1.8 → rsloop-0.1.9}/.cargo/config.toml +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/.github/workflows/tests.yml +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/.github/workflows/wheels.yml +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/.gitignore +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/LICENSE +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/build.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/demo/README.md +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/demo/fastapi_service.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/examples/01_basics.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/examples/02_fd_and_sockets.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/examples/03_streams.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/examples/04_unix_and_accepted_socket.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/examples/05_pipes_signals_subprocesses.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/rsloop.png +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/scripts/build-wheels.sh +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/scripts/generate-test-tls-certs.sh +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/scripts/python-versions.sh +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/scripts/test-supported-pythons.sh +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/async_event.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/blocking.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/context.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/errors.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/fast_streams.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/fd_ops.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/lib.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/loop_core.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/process_transport.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/python_api.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/python_names.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/runtime.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/tls.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/src/windows_vibeio.rs +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/tests/packages/aiohttp_test.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_compat.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_run.py +0 -0
- {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_tls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rsloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -268,7 +268,7 @@ Or manage the session manually:
|
|
|
268
268
|
```python
|
|
269
269
|
import rsloop
|
|
270
270
|
|
|
271
|
-
rsloop.start_profiler(
|
|
271
|
+
rsloop.start_profiler()
|
|
272
272
|
try:
|
|
273
273
|
rsloop.run(main())
|
|
274
274
|
finally:
|
|
@@ -279,10 +279,6 @@ This starts a Tracy client inside the process. Build a release binary, open
|
|
|
279
279
|
`tracy-profiler.exe`, then connect to the running process while the profiled
|
|
280
280
|
code is executing.
|
|
281
281
|
|
|
282
|
-
The `frequency` argument is accepted for compatibility with the old `pprof`
|
|
283
|
-
integration but is ignored by Tracy. `path` / `format` are also retained as
|
|
284
|
-
compatibility arguments on `stop_profiler()` / `profile()` and are ignored.
|
|
285
|
-
|
|
286
282
|
The current Tracy feature set is aimed at local Windows profiling:
|
|
287
283
|
`enable`, `only-localhost`, `sampling`, and `flush-on-exit`. The last one helps
|
|
288
284
|
short-lived runs flush data before exit.
|
|
@@ -236,7 +236,7 @@ Or manage the session manually:
|
|
|
236
236
|
```python
|
|
237
237
|
import rsloop
|
|
238
238
|
|
|
239
|
-
rsloop.start_profiler(
|
|
239
|
+
rsloop.start_profiler()
|
|
240
240
|
try:
|
|
241
241
|
rsloop.run(main())
|
|
242
242
|
finally:
|
|
@@ -247,10 +247,6 @@ This starts a Tracy client inside the process. Build a release binary, open
|
|
|
247
247
|
`tracy-profiler.exe`, then connect to the running process while the profiled
|
|
248
248
|
code is executing.
|
|
249
249
|
|
|
250
|
-
The `frequency` argument is accepted for compatibility with the old `pprof`
|
|
251
|
-
integration but is ignored by Tracy. `path` / `format` are also retained as
|
|
252
|
-
compatibility arguments on `stop_profiler()` / `profile()` and are ignored.
|
|
253
|
-
|
|
254
250
|
The current Tracy feature set is aimed at local Windows profiling:
|
|
255
251
|
`enable`, `only-localhost`, `sampling`, and `flush-on-exit`. The last one helps
|
|
256
252
|
short-lived runs flush data before exit.
|
|
@@ -33,7 +33,7 @@ uv run --with uvloop python benchmarks/compare_event_loops.py \
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
To launch an unmeasured Tracy session for each `rsloop` workload before the
|
|
36
|
-
measured runs, add a
|
|
36
|
+
measured runs, add a label directory:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
uv run --with maturin maturin develop --release --features profiler
|
|
@@ -43,9 +43,10 @@ uv run --with uvloop python benchmarks/compare_event_loops.py \
|
|
|
43
43
|
--profile-rsloop-dir benchmarks/profiles
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
No files are written by Tracy. The directory argument is only used to
|
|
47
|
-
unmeasured profiling pass before the warmup and
|
|
48
|
-
desktop profiler and connect while that pass is
|
|
46
|
+
No files are written by Tracy. The directory argument is only used to derive a
|
|
47
|
+
human-readable label for the unmeasured profiling pass before the warmup and
|
|
48
|
+
measured runs. Open the Tracy desktop profiler and connect while that pass is
|
|
49
|
+
running.
|
|
49
50
|
|
|
50
51
|
The runner executes each loop/workload in a fresh subprocess and reports:
|
|
51
52
|
|
|
@@ -13,7 +13,7 @@ import statistics
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
15
|
import time
|
|
16
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
17
|
from typing import Callable
|
|
18
18
|
|
|
19
19
|
|
|
@@ -37,7 +37,9 @@ class ChildResult:
|
|
|
37
37
|
workload: str
|
|
38
38
|
seconds: float
|
|
39
39
|
operations: int
|
|
40
|
+
baseline_rss_bytes: int
|
|
40
41
|
peak_rss_bytes: int
|
|
42
|
+
peak_rss_delta_bytes: int
|
|
41
43
|
|
|
42
44
|
@property
|
|
43
45
|
def ops_per_sec(self) -> float:
|
|
@@ -120,16 +122,10 @@ def parse_args() -> argparse.Namespace:
|
|
|
120
122
|
default=None,
|
|
121
123
|
help="Optional directory placeholder used to label one rsloop Tracy run per workload before measured runs",
|
|
122
124
|
)
|
|
123
|
-
parser.add_argument(
|
|
124
|
-
"--profile-frequency",
|
|
125
|
-
type=int,
|
|
126
|
-
default=999,
|
|
127
|
-
help="Compatibility option retained from the old pprof profiler; ignored by Tracy",
|
|
128
|
-
)
|
|
129
125
|
parser.add_argument("--child", action="store_true", help=argparse.SUPPRESS)
|
|
130
126
|
parser.add_argument("--loop", choices=LOOP_CHOICES, help=argparse.SUPPRESS)
|
|
131
127
|
parser.add_argument("--workload", choices=WORKLOAD_CHOICES, help=argparse.SUPPRESS)
|
|
132
|
-
parser.add_argument("--profile-
|
|
128
|
+
parser.add_argument("--profile-label", help=argparse.SUPPRESS)
|
|
133
129
|
return parser.parse_args()
|
|
134
130
|
|
|
135
131
|
|
|
@@ -158,8 +154,6 @@ def validate_args(args: argparse.Namespace) -> None:
|
|
|
158
154
|
raise SystemExit("--tcp-roundtrips must be > 0")
|
|
159
155
|
if args.payload_size <= 0:
|
|
160
156
|
raise SystemExit("--payload-size must be > 0")
|
|
161
|
-
if args.profile_frequency <= 0:
|
|
162
|
-
raise SystemExit("--profile-frequency must be > 0")
|
|
163
157
|
|
|
164
158
|
|
|
165
159
|
def env_flag(name: str) -> bool:
|
|
@@ -208,6 +202,7 @@ def is_loop_available(loop_name: str) -> tuple[bool, str | None]:
|
|
|
208
202
|
|
|
209
203
|
def get_peak_rss_bytes() -> int:
|
|
210
204
|
if sys.platform == "win32":
|
|
205
|
+
|
|
211
206
|
class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
|
|
212
207
|
_fields_ = [
|
|
213
208
|
("cb", wintypes.DWORD),
|
|
@@ -253,6 +248,25 @@ def get_peak_rss_bytes() -> int:
|
|
|
253
248
|
return int(peak_rss * 1024)
|
|
254
249
|
|
|
255
250
|
|
|
251
|
+
def get_current_rss_bytes() -> int:
|
|
252
|
+
if sys.platform != "linux":
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
with open("/proc/self/status", "r", encoding="utf-8") as f:
|
|
257
|
+
for line in f:
|
|
258
|
+
if not line.startswith("VmRSS:"):
|
|
259
|
+
continue
|
|
260
|
+
parts = line.split()
|
|
261
|
+
if len(parts) < 2:
|
|
262
|
+
break
|
|
263
|
+
return int(parts[1]) * 1024
|
|
264
|
+
except OSError:
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
256
270
|
def format_bytes(num_bytes: int) -> str:
|
|
257
271
|
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
|
258
272
|
value = float(num_bytes)
|
|
@@ -300,7 +314,9 @@ async def bench_callbacks(loop_name: str, iterations: int) -> ChildResult:
|
|
|
300
314
|
"callbacks",
|
|
301
315
|
time.perf_counter() - start,
|
|
302
316
|
iterations,
|
|
317
|
+
baseline_rss_bytes=0,
|
|
303
318
|
peak_rss_bytes=0,
|
|
319
|
+
peak_rss_delta_bytes=0,
|
|
304
320
|
)
|
|
305
321
|
|
|
306
322
|
|
|
@@ -319,7 +335,9 @@ async def bench_tasks(loop_name: str, iterations: int, batch_size: int) -> Child
|
|
|
319
335
|
"tasks",
|
|
320
336
|
time.perf_counter() - start,
|
|
321
337
|
iterations,
|
|
338
|
+
baseline_rss_bytes=0,
|
|
322
339
|
peak_rss_bytes=0,
|
|
340
|
+
peak_rss_delta_bytes=0,
|
|
323
341
|
)
|
|
324
342
|
|
|
325
343
|
|
|
@@ -372,7 +390,9 @@ async def bench_tcp_streams(
|
|
|
372
390
|
"tcp_streams",
|
|
373
391
|
duration,
|
|
374
392
|
roundtrips,
|
|
393
|
+
baseline_rss_bytes=0,
|
|
375
394
|
peak_rss_bytes=0,
|
|
395
|
+
peak_rss_delta_bytes=0,
|
|
376
396
|
)
|
|
377
397
|
finally:
|
|
378
398
|
server.close()
|
|
@@ -382,6 +402,7 @@ async def bench_tcp_streams(
|
|
|
382
402
|
def child_main(args: argparse.Namespace) -> int:
|
|
383
403
|
gc.collect()
|
|
384
404
|
gc.disable()
|
|
405
|
+
baseline_rss_bytes = 0
|
|
385
406
|
try:
|
|
386
407
|
if args.workload == "callbacks":
|
|
387
408
|
coro = bench_callbacks(args.loop, args.callbacks)
|
|
@@ -392,10 +413,12 @@ def child_main(args: argparse.Namespace) -> int:
|
|
|
392
413
|
else: # pragma: no cover - parser guards this
|
|
393
414
|
raise AssertionError(f"unsupported workload: {args.workload}")
|
|
394
415
|
|
|
395
|
-
profile_requested = args.
|
|
416
|
+
profile_requested = args.profile_label is not None or (
|
|
396
417
|
args.loop == "rsloop" and env_flag(RSLOOP_PROFILE_ENV)
|
|
397
418
|
)
|
|
398
|
-
|
|
419
|
+
loop_factory_for(args.loop)
|
|
420
|
+
baseline_rss_bytes = get_current_rss_bytes()
|
|
421
|
+
if profile_requested and not args.profile_label:
|
|
399
422
|
print(
|
|
400
423
|
f"[profile] Tracy enabled via {RSLOOP_PROFILE_ENV}=1 for {args.loop}/{args.workload}",
|
|
401
424
|
flush=True,
|
|
@@ -404,19 +427,24 @@ def child_main(args: argparse.Namespace) -> int:
|
|
|
404
427
|
if args.loop != "rsloop":
|
|
405
428
|
raise RuntimeError("profiling is only supported for rsloop")
|
|
406
429
|
rsloop = importlib.import_module("rsloop")
|
|
407
|
-
|
|
430
|
+
if args.profile_label:
|
|
431
|
+
print(f"[profile] Tracy session label: {args.profile_label}", flush=True)
|
|
432
|
+
with rsloop.profile():
|
|
408
433
|
result = run_with_loop(args.loop, coro)
|
|
409
434
|
else:
|
|
410
435
|
result = run_with_loop(args.loop, coro)
|
|
411
436
|
finally:
|
|
412
437
|
gc.enable()
|
|
413
438
|
|
|
414
|
-
result =
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
seconds=result.seconds,
|
|
418
|
-
operations=result.operations,
|
|
439
|
+
result = replace(
|
|
440
|
+
result,
|
|
441
|
+
baseline_rss_bytes=baseline_rss_bytes,
|
|
419
442
|
peak_rss_bytes=get_peak_rss_bytes(),
|
|
443
|
+
peak_rss_delta_bytes=0,
|
|
444
|
+
)
|
|
445
|
+
result = replace(
|
|
446
|
+
result,
|
|
447
|
+
peak_rss_delta_bytes=max(0, result.peak_rss_bytes - result.baseline_rss_bytes),
|
|
420
448
|
)
|
|
421
449
|
|
|
422
450
|
print(
|
|
@@ -427,7 +455,9 @@ def child_main(args: argparse.Namespace) -> int:
|
|
|
427
455
|
"seconds": result.seconds,
|
|
428
456
|
"operations": result.operations,
|
|
429
457
|
"ops_per_sec": result.ops_per_sec,
|
|
458
|
+
"baseline_rss_bytes": result.baseline_rss_bytes,
|
|
430
459
|
"peak_rss_bytes": result.peak_rss_bytes,
|
|
460
|
+
"peak_rss_delta_bytes": result.peak_rss_delta_bytes,
|
|
431
461
|
}
|
|
432
462
|
)
|
|
433
463
|
)
|
|
@@ -440,7 +470,7 @@ def run_child(
|
|
|
440
470
|
workload: str,
|
|
441
471
|
args: argparse.Namespace,
|
|
442
472
|
*,
|
|
443
|
-
|
|
473
|
+
profile_label: str | None = None,
|
|
444
474
|
) -> ChildResult:
|
|
445
475
|
cmd = [
|
|
446
476
|
sys.executable,
|
|
@@ -460,11 +490,9 @@ def run_child(
|
|
|
460
490
|
str(args.tcp_roundtrips),
|
|
461
491
|
"--payload-size",
|
|
462
492
|
str(args.payload_size),
|
|
463
|
-
"--profile-frequency",
|
|
464
|
-
str(args.profile_frequency),
|
|
465
493
|
]
|
|
466
|
-
if
|
|
467
|
-
cmd.extend(["--profile-
|
|
494
|
+
if profile_label is not None:
|
|
495
|
+
cmd.extend(["--profile-label", profile_label])
|
|
468
496
|
env = os.environ.copy()
|
|
469
497
|
if loop_name == "rsloop":
|
|
470
498
|
env["RSLOOP_USE_FAST_STREAMS"] = "1" if args.rsloop_fast_streams else "0"
|
|
@@ -492,17 +520,23 @@ def run_child(
|
|
|
492
520
|
workload=payload["workload"],
|
|
493
521
|
seconds=payload["seconds"],
|
|
494
522
|
operations=payload["operations"],
|
|
523
|
+
baseline_rss_bytes=payload.get("baseline_rss_bytes", 0),
|
|
495
524
|
peak_rss_bytes=payload["peak_rss_bytes"],
|
|
525
|
+
peak_rss_delta_bytes=payload.get("peak_rss_delta_bytes", 0),
|
|
496
526
|
)
|
|
497
527
|
|
|
498
528
|
|
|
499
529
|
def print_workload_table(workload: str, runs: dict[str, list[ChildResult]]) -> None:
|
|
500
|
-
rows: list[tuple[str, float, float, float, int, int]] = []
|
|
530
|
+
rows: list[tuple[str, float, float, float, int, int, int, int]] = []
|
|
501
531
|
for loop_name, loop_runs in runs.items():
|
|
502
532
|
seconds = [item.seconds for item in loop_runs]
|
|
533
|
+
baseline_rss_values = [item.baseline_rss_bytes for item in loop_runs]
|
|
503
534
|
peak_rss_values = [item.peak_rss_bytes for item in loop_runs]
|
|
535
|
+
peak_rss_delta_values = [item.peak_rss_delta_bytes for item in loop_runs]
|
|
504
536
|
median_seconds = statistics.median(seconds)
|
|
537
|
+
median_baseline_rss = int(statistics.median(baseline_rss_values))
|
|
505
538
|
median_peak_rss = int(statistics.median(peak_rss_values))
|
|
539
|
+
median_peak_rss_delta = int(statistics.median(peak_rss_delta_values))
|
|
506
540
|
operations = loop_runs[0].operations
|
|
507
541
|
ops_per_sec = (
|
|
508
542
|
operations / median_seconds if median_seconds > 0 else float("inf")
|
|
@@ -514,7 +548,9 @@ def print_workload_table(workload: str, runs: dict[str, list[ChildResult]]) -> N
|
|
|
514
548
|
ops_per_sec,
|
|
515
549
|
min(seconds),
|
|
516
550
|
operations,
|
|
551
|
+
median_baseline_rss,
|
|
517
552
|
median_peak_rss,
|
|
553
|
+
median_peak_rss_delta,
|
|
518
554
|
)
|
|
519
555
|
)
|
|
520
556
|
|
|
@@ -523,19 +559,45 @@ def print_workload_table(workload: str, runs: dict[str, list[ChildResult]]) -> N
|
|
|
523
559
|
|
|
524
560
|
print()
|
|
525
561
|
print(f"{workload} ({rows[0][4]:,} ops)")
|
|
526
|
-
|
|
527
|
-
f"{'loop':<10} {'median_s':>12} {'best_s':>12} {'ops_per_s':>14} {'peak_rss':>12} {'vs_fastest':>12}"
|
|
528
|
-
)
|
|
529
|
-
for loop_name, median_seconds, ops_per_sec, best_seconds, _, median_peak_rss in rows:
|
|
530
|
-
relative = median_seconds / fastest if fastest > 0 else 1.0
|
|
562
|
+
if sys.platform == "linux":
|
|
531
563
|
print(
|
|
532
|
-
f"{
|
|
533
|
-
f"{median_seconds:>12.6f} "
|
|
534
|
-
f"{best_seconds:>12.6f} "
|
|
535
|
-
f"{ops_per_sec:>14,.0f} "
|
|
536
|
-
f"{format_bytes(median_peak_rss):>12} "
|
|
537
|
-
f"{relative:>11.2f}x"
|
|
564
|
+
f"{'loop':<10} {'median_s':>12} {'best_s':>12} {'ops_per_s':>14} {'baseline_rss':>14} {'peak_rss':>12} {'peak_delta':>12} {'vs_fastest':>12}"
|
|
538
565
|
)
|
|
566
|
+
else:
|
|
567
|
+
print(
|
|
568
|
+
f"{'loop':<10} {'median_s':>12} {'best_s':>12} {'ops_per_s':>14} {'peak_rss':>12} {'vs_fastest':>12}"
|
|
569
|
+
)
|
|
570
|
+
for (
|
|
571
|
+
loop_name,
|
|
572
|
+
median_seconds,
|
|
573
|
+
ops_per_sec,
|
|
574
|
+
best_seconds,
|
|
575
|
+
_,
|
|
576
|
+
median_baseline_rss,
|
|
577
|
+
median_peak_rss,
|
|
578
|
+
median_peak_rss_delta,
|
|
579
|
+
) in rows:
|
|
580
|
+
relative = median_seconds / fastest if fastest > 0 else 1.0
|
|
581
|
+
if sys.platform == "linux":
|
|
582
|
+
print(
|
|
583
|
+
f"{loop_name:<10} "
|
|
584
|
+
f"{median_seconds:>12.6f} "
|
|
585
|
+
f"{best_seconds:>12.6f} "
|
|
586
|
+
f"{ops_per_sec:>14,.0f} "
|
|
587
|
+
f"{format_bytes(median_baseline_rss):>14} "
|
|
588
|
+
f"{format_bytes(median_peak_rss):>12} "
|
|
589
|
+
f"{format_bytes(median_peak_rss_delta):>12} "
|
|
590
|
+
f"{relative:>11.2f}x"
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
print(
|
|
594
|
+
f"{loop_name:<10} "
|
|
595
|
+
f"{median_seconds:>12.6f} "
|
|
596
|
+
f"{best_seconds:>12.6f} "
|
|
597
|
+
f"{ops_per_sec:>14,.0f} "
|
|
598
|
+
f"{format_bytes(median_peak_rss):>12} "
|
|
599
|
+
f"{relative:>11.2f}x"
|
|
600
|
+
)
|
|
539
601
|
|
|
540
602
|
|
|
541
603
|
def parent_main(args: argparse.Namespace) -> int:
|
|
@@ -584,16 +646,14 @@ def parent_main(args: argparse.Namespace) -> int:
|
|
|
584
646
|
print(f"Running {workload} on {loop_name}...")
|
|
585
647
|
if profile_rsloop_dir and loop_name == "rsloop":
|
|
586
648
|
os.makedirs(profile_rsloop_dir, exist_ok=True)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
)
|
|
590
|
-
print(f" starting Tracy session labeled {profile_output}")
|
|
649
|
+
profile_label = os.path.join(profile_rsloop_dir, f"rsloop-{workload}")
|
|
650
|
+
print(f" starting Tracy session labeled {profile_label}")
|
|
591
651
|
run_child(
|
|
592
652
|
script_path,
|
|
593
653
|
loop_name,
|
|
594
654
|
workload,
|
|
595
655
|
args,
|
|
596
|
-
|
|
656
|
+
profile_label=profile_label,
|
|
597
657
|
)
|
|
598
658
|
for _ in range(args.warmups):
|
|
599
659
|
run_child(script_path, loop_name, workload, args)
|
|
@@ -611,7 +671,9 @@ def parent_main(args: argparse.Namespace) -> int:
|
|
|
611
671
|
"seconds": item.seconds,
|
|
612
672
|
"operations": item.operations,
|
|
613
673
|
"ops_per_sec": item.ops_per_sec,
|
|
674
|
+
"baseline_rss_bytes": item.baseline_rss_bytes,
|
|
614
675
|
"peak_rss_bytes": item.peak_rss_bytes,
|
|
676
|
+
"peak_rss_delta_bytes": item.peak_rss_delta_bytes,
|
|
615
677
|
}
|
|
616
678
|
for item in measured
|
|
617
679
|
],
|
|
@@ -105,32 +105,24 @@ def profiler_running() -> bool:
|
|
|
105
105
|
return __profiler_running()
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def start_profiler(
|
|
109
|
-
|
|
108
|
+
def start_profiler() -> None:
|
|
109
|
+
"""Start a Tracy profiling session."""
|
|
110
|
+
__start_profiler()
|
|
110
111
|
|
|
111
112
|
|
|
112
|
-
def stop_profiler(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
format: str | None = None,
|
|
116
|
-
) -> str | None:
|
|
117
|
-
resolved = None if path is None else __os.fspath(path)
|
|
118
|
-
return __stop_profiler(resolved, format=format)
|
|
113
|
+
def stop_profiler() -> None:
|
|
114
|
+
"""Stop the active Tracy profiling session."""
|
|
115
|
+
__stop_profiler()
|
|
119
116
|
|
|
120
117
|
|
|
121
118
|
@__contextlib.contextmanager
|
|
122
|
-
def profile(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
frequency: int = 999,
|
|
126
|
-
format: str | None = None,
|
|
127
|
-
) -> __typing.Iterator[str | None]:
|
|
128
|
-
resolved_path = None if path is None else __os.fspath(path)
|
|
129
|
-
start_profiler(frequency=frequency)
|
|
119
|
+
def profile() -> __typing.Iterator[None]:
|
|
120
|
+
"""Context manager wrapper around ``start_profiler()`` / ``stop_profiler()``."""
|
|
121
|
+
start_profiler()
|
|
130
122
|
try:
|
|
131
|
-
yield
|
|
123
|
+
yield None
|
|
132
124
|
finally:
|
|
133
|
-
stop_profiler(
|
|
125
|
+
stop_profiler()
|
|
134
126
|
|
|
135
127
|
|
|
136
128
|
__ORIG_OPEN_CONNECTION = __asyncio.open_connection
|
|
@@ -33,6 +33,7 @@ pub struct ReadyCallback {
|
|
|
33
33
|
kind: CallbackKind,
|
|
34
34
|
callback: Py<PyAny>,
|
|
35
35
|
args: CallbackArgs,
|
|
36
|
+
args_tuple: Py<PyTuple>,
|
|
36
37
|
context: Py<PyAny>,
|
|
37
38
|
context_needs_run: bool,
|
|
38
39
|
cancelled: AtomicBool,
|
|
@@ -45,19 +46,20 @@ impl ReadyCallback {
|
|
|
45
46
|
id: CallbackId,
|
|
46
47
|
kind: CallbackKind,
|
|
47
48
|
callback: Py<PyAny>,
|
|
48
|
-
|
|
49
|
+
args_tuple: Py<PyTuple>,
|
|
49
50
|
context: Py<PyAny>,
|
|
50
51
|
context_needs_run: bool,
|
|
51
52
|
) -> Self {
|
|
52
|
-
let args = match
|
|
53
|
+
let args = match args_tuple.bind(py).len() {
|
|
53
54
|
0 => CallbackArgs::None,
|
|
54
55
|
1 => CallbackArgs::One(
|
|
55
|
-
|
|
56
|
+
args_tuple
|
|
57
|
+
.bind(py)
|
|
56
58
|
.get_item(0)
|
|
57
59
|
.expect("single callback arg")
|
|
58
60
|
.unbind(),
|
|
59
61
|
),
|
|
60
|
-
_ => CallbackArgs::Many(
|
|
62
|
+
_ => CallbackArgs::Many(args_tuple.clone_ref(py)),
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
Self {
|
|
@@ -65,6 +67,7 @@ impl ReadyCallback {
|
|
|
65
67
|
kind,
|
|
66
68
|
callback,
|
|
67
69
|
args,
|
|
70
|
+
args_tuple,
|
|
68
71
|
context,
|
|
69
72
|
context_needs_run,
|
|
70
73
|
cancelled: AtomicBool::new(false),
|
|
@@ -146,13 +149,7 @@ impl ReadyCallback {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
pub fn clone_args_tuple(&self, py: Python<'_>) -> Py<PyTuple> {
|
|
149
|
-
|
|
150
|
-
CallbackArgs::None => PyTuple::empty(py).unbind(),
|
|
151
|
-
CallbackArgs::One(arg) => PyTuple::new(py, [arg.bind(py)])
|
|
152
|
-
.expect("single callback arg tuple")
|
|
153
|
-
.unbind(),
|
|
154
|
-
CallbackArgs::Many(args) => args.clone_ref(py),
|
|
155
|
-
}
|
|
152
|
+
self.args_tuple.clone_ref(py)
|
|
156
153
|
}
|
|
157
154
|
|
|
158
155
|
#[inline]
|
|
@@ -21,23 +21,8 @@ mod imp {
|
|
|
21
21
|
ACTIVE_PROFILER.get_or_init(|| Mutex::new(None))
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return Err(PyValueError::new_err(format!(
|
|
27
|
-
"Tracy does not support file output formats; got format={format:?}"
|
|
28
|
-
)));
|
|
29
|
-
}
|
|
30
|
-
Ok(())
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
#[pyfunction(signature = (*, frequency = 999))]
|
|
34
|
-
pub fn start_profiler(frequency: i32) -> PyResult<()> {
|
|
35
|
-
if frequency <= 0 {
|
|
36
|
-
return Err(PyValueError::new_err(
|
|
37
|
-
"profiler frequency must be greater than zero",
|
|
38
|
-
));
|
|
39
|
-
}
|
|
40
|
-
|
|
24
|
+
#[pyfunction]
|
|
25
|
+
pub fn start_profiler() -> PyResult<()> {
|
|
41
26
|
let mut active = active_profiler()
|
|
42
27
|
.lock()
|
|
43
28
|
.map_err(|_| PyRuntimeError::new_err("profiler state mutex is poisoned"))?;
|
|
@@ -45,7 +30,6 @@ mod imp {
|
|
|
45
30
|
return Err(PyRuntimeError::new_err("profiler is already running"));
|
|
46
31
|
}
|
|
47
32
|
|
|
48
|
-
let _ = frequency;
|
|
49
33
|
let client = Client::start();
|
|
50
34
|
client.set_thread_name("python-main");
|
|
51
35
|
let session_span = client.clone().span_alloc(
|
|
@@ -70,10 +54,8 @@ mod imp {
|
|
|
70
54
|
.unwrap_or(false)
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
#[pyfunction
|
|
74
|
-
pub fn stop_profiler(
|
|
75
|
-
ensure_ignored_format(format)?;
|
|
76
|
-
|
|
57
|
+
#[pyfunction]
|
|
58
|
+
pub fn stop_profiler() -> PyResult<()> {
|
|
77
59
|
let active = active_profiler()
|
|
78
60
|
.lock()
|
|
79
61
|
.map_err(|_| PyRuntimeError::new_err("profiler state mutex is poisoned"))?
|
|
@@ -83,7 +65,7 @@ mod imp {
|
|
|
83
65
|
slot.borrow_mut().take();
|
|
84
66
|
});
|
|
85
67
|
drop(active.client);
|
|
86
|
-
Ok(
|
|
68
|
+
Ok(())
|
|
87
69
|
}
|
|
88
70
|
}
|
|
89
71
|
|
|
@@ -100,15 +82,13 @@ mod imp {
|
|
|
100
82
|
false
|
|
101
83
|
}
|
|
102
84
|
|
|
103
|
-
#[pyfunction
|
|
104
|
-
pub fn start_profiler(
|
|
105
|
-
let _ = frequency;
|
|
85
|
+
#[pyfunction]
|
|
86
|
+
pub fn start_profiler() -> PyResult<()> {
|
|
106
87
|
Err(PyRuntimeError::new_err(PROFILER_DISABLED_MESSAGE))
|
|
107
88
|
}
|
|
108
89
|
|
|
109
|
-
#[pyfunction
|
|
110
|
-
pub fn stop_profiler(
|
|
111
|
-
let _ = (path, format);
|
|
90
|
+
#[pyfunction]
|
|
91
|
+
pub fn stop_profiler() -> PyResult<()> {
|
|
112
92
|
Err(PyRuntimeError::new_err(PROFILER_DISABLED_MESSAGE))
|
|
113
93
|
}
|
|
114
94
|
}
|
|
@@ -815,6 +815,7 @@ impl StreamTransportCore {
|
|
|
815
815
|
|
|
816
816
|
pub(crate) fn drain_pending_read_events_with_py(&self, py: Python<'_>) -> PyResult<()> {
|
|
817
817
|
profiling::scope!("StreamTransportCore::drain_pending_read_events_with_py");
|
|
818
|
+
let mut pending_data: Option<Vec<u8>> = None;
|
|
818
819
|
loop {
|
|
819
820
|
let mut pending = {
|
|
820
821
|
let mut queue = self
|
|
@@ -831,8 +832,6 @@ impl StreamTransportCore {
|
|
|
831
832
|
drained
|
|
832
833
|
};
|
|
833
834
|
|
|
834
|
-
let mut pending_data: Option<Vec<u8>> = None;
|
|
835
|
-
|
|
836
835
|
while let Some(event) = pending.pop_front() {
|
|
837
836
|
match event {
|
|
838
837
|
PendingReadEvent::Data(data) => {
|
|
@@ -1051,7 +1050,20 @@ impl StreamTransportCore {
|
|
|
1051
1050
|
|
|
1052
1051
|
fn data_received_with_py(&self, py: Python<'_>, data: &[u8]) -> PyResult<()> {
|
|
1053
1052
|
profiling::scope!("StreamTransportCore::data_received_with_py");
|
|
1054
|
-
let
|
|
1053
|
+
let fast_path = {
|
|
1054
|
+
let state = self.state.lock().expect("poisoned transport state");
|
|
1055
|
+
state
|
|
1056
|
+
.callbacks
|
|
1057
|
+
.stream_reader_fast_path
|
|
1058
|
+
.as_ref()
|
|
1059
|
+
.map(|value| value.clone_ref(py))
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
if let Some(fast_path) = fast_path.as_ref() {
|
|
1063
|
+
return fast_path.feed_data(py, data);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
let (data_received, get_buffer, buffer_updated, context, context_needs_run) = {
|
|
1055
1067
|
let state = self.state.lock().expect("poisoned transport state");
|
|
1056
1068
|
(
|
|
1057
1069
|
state
|
|
@@ -1069,20 +1081,11 @@ impl StreamTransportCore {
|
|
|
1069
1081
|
.buffer_updated
|
|
1070
1082
|
.as_ref()
|
|
1071
1083
|
.map(|value| value.clone_ref(py)),
|
|
1072
|
-
state
|
|
1073
|
-
.callbacks
|
|
1074
|
-
.stream_reader_fast_path
|
|
1075
|
-
.as_ref()
|
|
1076
|
-
.map(|value| value.clone_ref(py)),
|
|
1077
1084
|
state.context.clone_ref(py),
|
|
1078
1085
|
state.context_needs_run,
|
|
1079
1086
|
)
|
|
1080
1087
|
};
|
|
1081
1088
|
|
|
1082
|
-
if let Some(fast_path) = fast_path.as_ref() {
|
|
1083
|
-
return fast_path.feed_data(py, data);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1089
|
if let (Some(get_buffer), Some(buffer_updated)) =
|
|
1087
1090
|
(get_buffer.as_ref(), buffer_updated.as_ref())
|
|
1088
1091
|
{
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|