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.
Files changed (49) hide show
  1. {rsloop-0.1.8 → rsloop-0.1.9}/Cargo.lock +1 -1
  2. {rsloop-0.1.8 → rsloop-0.1.9}/Cargo.toml +1 -1
  3. {rsloop-0.1.8 → rsloop-0.1.9}/PKG-INFO +2 -6
  4. {rsloop-0.1.8 → rsloop-0.1.9}/README.md +1 -5
  5. {rsloop-0.1.8 → rsloop-0.1.9}/benchmarks/README.md +5 -4
  6. {rsloop-0.1.8 → rsloop-0.1.9}/benchmarks/compare_event_loops.py +102 -40
  7. {rsloop-0.1.8 → rsloop-0.1.9}/justfile +4 -0
  8. {rsloop-0.1.8 → rsloop-0.1.9}/pyproject.toml +1 -1
  9. {rsloop-0.1.8 → rsloop-0.1.9}/python/rsloop/__init__.py +11 -19
  10. {rsloop-0.1.8 → rsloop-0.1.9}/src/callbacks.rs +8 -11
  11. {rsloop-0.1.8 → rsloop-0.1.9}/src/profiler.rs +9 -29
  12. {rsloop-0.1.8 → rsloop-0.1.9}/src/stream_transport.rs +15 -12
  13. {rsloop-0.1.8 → rsloop-0.1.9}/uv.lock +1 -1
  14. {rsloop-0.1.8 → rsloop-0.1.9}/.cargo/config.toml +0 -0
  15. {rsloop-0.1.8 → rsloop-0.1.9}/.github/workflows/tests.yml +0 -0
  16. {rsloop-0.1.8 → rsloop-0.1.9}/.github/workflows/wheels.yml +0 -0
  17. {rsloop-0.1.8 → rsloop-0.1.9}/.gitignore +0 -0
  18. {rsloop-0.1.8 → rsloop-0.1.9}/LICENSE +0 -0
  19. {rsloop-0.1.8 → rsloop-0.1.9}/build.rs +0 -0
  20. {rsloop-0.1.8 → rsloop-0.1.9}/demo/README.md +0 -0
  21. {rsloop-0.1.8 → rsloop-0.1.9}/demo/fastapi_service.py +0 -0
  22. {rsloop-0.1.8 → rsloop-0.1.9}/examples/01_basics.py +0 -0
  23. {rsloop-0.1.8 → rsloop-0.1.9}/examples/02_fd_and_sockets.py +0 -0
  24. {rsloop-0.1.8 → rsloop-0.1.9}/examples/03_streams.py +0 -0
  25. {rsloop-0.1.8 → rsloop-0.1.9}/examples/04_unix_and_accepted_socket.py +0 -0
  26. {rsloop-0.1.8 → rsloop-0.1.9}/examples/05_pipes_signals_subprocesses.py +0 -0
  27. {rsloop-0.1.8 → rsloop-0.1.9}/rsloop.png +0 -0
  28. {rsloop-0.1.8 → rsloop-0.1.9}/scripts/build-wheels.sh +0 -0
  29. {rsloop-0.1.8 → rsloop-0.1.9}/scripts/generate-test-tls-certs.sh +0 -0
  30. {rsloop-0.1.8 → rsloop-0.1.9}/scripts/python-versions.sh +0 -0
  31. {rsloop-0.1.8 → rsloop-0.1.9}/scripts/test-supported-pythons.sh +0 -0
  32. {rsloop-0.1.8 → rsloop-0.1.9}/src/async_event.rs +0 -0
  33. {rsloop-0.1.8 → rsloop-0.1.9}/src/blocking.rs +0 -0
  34. {rsloop-0.1.8 → rsloop-0.1.9}/src/context.rs +0 -0
  35. {rsloop-0.1.8 → rsloop-0.1.9}/src/errors.rs +0 -0
  36. {rsloop-0.1.8 → rsloop-0.1.9}/src/fast_streams.rs +0 -0
  37. {rsloop-0.1.8 → rsloop-0.1.9}/src/fd_ops.rs +0 -0
  38. {rsloop-0.1.8 → rsloop-0.1.9}/src/lib.rs +0 -0
  39. {rsloop-0.1.8 → rsloop-0.1.9}/src/loop_core.rs +0 -0
  40. {rsloop-0.1.8 → rsloop-0.1.9}/src/process_transport.rs +0 -0
  41. {rsloop-0.1.8 → rsloop-0.1.9}/src/python_api.rs +0 -0
  42. {rsloop-0.1.8 → rsloop-0.1.9}/src/python_names.rs +0 -0
  43. {rsloop-0.1.8 → rsloop-0.1.9}/src/runtime.rs +0 -0
  44. {rsloop-0.1.8 → rsloop-0.1.9}/src/tls.rs +0 -0
  45. {rsloop-0.1.8 → rsloop-0.1.9}/src/windows_vibeio.rs +0 -0
  46. {rsloop-0.1.8 → rsloop-0.1.9}/tests/packages/aiohttp_test.py +0 -0
  47. {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_compat.py +0 -0
  48. {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_run.py +0 -0
  49. {rsloop-0.1.8 → rsloop-0.1.9}/tests/test_tls.py +0 -0
@@ -1080,7 +1080,7 @@ dependencies = [
1080
1080
 
1081
1081
  [[package]]
1082
1082
  name = "rsloop"
1083
- version = "0.1.8"
1083
+ version = "0.1.9"
1084
1084
  dependencies = [
1085
1085
  "async-std",
1086
1086
  "compio",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rsloop"
3
- version = "0.1.8"
3
+ version = "0.1.9"
4
4
  edition = "2021"
5
5
  description = "An event loop for asyncio written in Rust"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rsloop
3
- Version: 0.1.8
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(frequency=999)
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(frequency=999)
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 profile directory placeholder:
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 label the
47
- unmeasured profiling pass before the warmup and measured runs. Open the Tracy
48
- desktop profiler and connect while that pass is running.
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-output", help=argparse.SUPPRESS)
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.profile_output is not None or (
416
+ profile_requested = args.profile_label is not None or (
396
417
  args.loop == "rsloop" and env_flag(RSLOOP_PROFILE_ENV)
397
418
  )
398
- if profile_requested and not args.profile_output:
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
- with rsloop.profile(args.profile_output, frequency=args.profile_frequency):
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 = ChildResult(
415
- loop=result.loop,
416
- workload=result.workload,
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
- profile_output: str | None = None,
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 profile_output is not None:
467
- cmd.extend(["--profile-output", profile_output])
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
- print(
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"{loop_name:<10} "
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
- profile_output = os.path.join(
588
- profile_rsloop_dir, f"rsloop-{workload}.svg"
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
- profile_output=profile_output,
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
  ],
@@ -3,5 +3,9 @@ set shell := ["bash", "-euo", "pipefail", "-c"]
3
3
  tls-test-certs outdir="tests/fixtures/tls":
4
4
  ./scripts/generate-test-tls-certs.sh {{outdir}}
5
5
 
6
+ fmt:
7
+ uv run ruff format .
8
+ cargo fmt --all
9
+
6
10
  test:
7
11
  uv run python -m unittest discover -s tests
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rsloop"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "An event loop for asyncio written in Rust"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -105,32 +105,24 @@ def profiler_running() -> bool:
105
105
  return __profiler_running()
106
106
 
107
107
 
108
- def start_profiler(*, frequency: int = 999) -> None:
109
- __start_profiler(frequency=frequency)
108
+ def start_profiler() -> None:
109
+ """Start a Tracy profiling session."""
110
+ __start_profiler()
110
111
 
111
112
 
112
- def stop_profiler(
113
- path: str | __os.PathLike[str] | None = None,
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
- path: str | __os.PathLike[str] | None = None,
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 resolved_path
123
+ yield None
132
124
  finally:
133
- stop_profiler(resolved_path, format=format)
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
- args: Py<PyTuple>,
49
+ args_tuple: Py<PyTuple>,
49
50
  context: Py<PyAny>,
50
51
  context_needs_run: bool,
51
52
  ) -> Self {
52
- let args = match args.bind(py).len() {
53
+ let args = match args_tuple.bind(py).len() {
53
54
  0 => CallbackArgs::None,
54
55
  1 => CallbackArgs::One(
55
- args.bind(py)
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(args),
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
- match &self.args {
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
- fn ensure_ignored_format(format: Option<&str>) -> PyResult<()> {
25
- if let Some(format) = format {
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(signature = (path = None, *, format = None))]
74
- pub fn stop_profiler(path: Option<String>, format: Option<&str>) -> PyResult<Option<String>> {
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(path)
68
+ Ok(())
87
69
  }
88
70
  }
89
71
 
@@ -100,15 +82,13 @@ mod imp {
100
82
  false
101
83
  }
102
84
 
103
- #[pyfunction(signature = (*, frequency = 999))]
104
- pub fn start_profiler(frequency: i32) -> PyResult<()> {
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(signature = (path = None, *, format = None))]
110
- pub fn stop_profiler(path: Option<String>, format: Option<&str>) -> PyResult<Option<String>> {
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 (data_received, get_buffer, buffer_updated, fast_path, context, context_needs_run) = {
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
  {
@@ -4,5 +4,5 @@ requires-python = ">=3.8"
4
4
 
5
5
  [[package]]
6
6
  name = "rsloop"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  source = { editable = "." }
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