Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ Or manage the session manually:
```python
import rsloop

rsloop.start_profiler(frequency=999)
rsloop.start_profiler()
try:
rsloop.run(main())
finally:
Expand All @@ -247,10 +247,6 @@ This starts a Tracy client inside the process. Build a release binary, open
`tracy-profiler.exe`, then connect to the running process while the profiled
code is executing.

The `frequency` argument is accepted for compatibility with the old `pprof`
integration but is ignored by Tracy. `path` / `format` are also retained as
compatibility arguments on `stop_profiler()` / `profile()` and are ignored.

The current Tracy feature set is aimed at local Windows profiling:
`enable`, `only-localhost`, `sampling`, and `flush-on-exit`. The last one helps
short-lived runs flush data before exit.
Expand Down
9 changes: 5 additions & 4 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ uv run --with uvloop python benchmarks/compare_event_loops.py \
```

To launch an unmeasured Tracy session for each `rsloop` workload before the
measured runs, add a profile directory placeholder:
measured runs, add a label directory:

```bash
uv run --with maturin maturin develop --release --features profiler
Expand All @@ -43,9 +43,10 @@ uv run --with uvloop python benchmarks/compare_event_loops.py \
--profile-rsloop-dir benchmarks/profiles
```

No files are written by Tracy. The directory argument is only used to label the
unmeasured profiling pass before the warmup and measured runs. Open the Tracy
desktop profiler and connect while that pass is running.
No files are written by Tracy. The directory argument is only used to derive a
human-readable label for the unmeasured profiling pass before the warmup and
measured runs. Open the Tracy desktop profiler and connect while that pass is
running.

The runner executes each loop/workload in a fresh subprocess and reports:

Expand Down
52 changes: 17 additions & 35 deletions benchmarks/compare_event_loops.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import subprocess
import sys
import time
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Callable


Expand Down Expand Up @@ -122,16 +122,10 @@ def parse_args() -> argparse.Namespace:
default=None,
help="Optional directory placeholder used to label one rsloop Tracy run per workload before measured runs",
)
parser.add_argument(
"--profile-frequency",
type=int,
default=999,
help="Compatibility option retained from the old pprof profiler; ignored by Tracy",
)
parser.add_argument("--child", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--loop", choices=LOOP_CHOICES, help=argparse.SUPPRESS)
parser.add_argument("--workload", choices=WORKLOAD_CHOICES, help=argparse.SUPPRESS)
parser.add_argument("--profile-output", help=argparse.SUPPRESS)
parser.add_argument("--profile-label", help=argparse.SUPPRESS)
return parser.parse_args()


Expand Down Expand Up @@ -160,8 +154,6 @@ def validate_args(args: argparse.Namespace) -> None:
raise SystemExit("--tcp-roundtrips must be > 0")
if args.payload_size <= 0:
raise SystemExit("--payload-size must be > 0")
if args.profile_frequency <= 0:
raise SystemExit("--profile-frequency must be > 0")


def env_flag(name: str) -> bool:
Expand Down Expand Up @@ -421,12 +413,12 @@ def child_main(args: argparse.Namespace) -> int:
else: # pragma: no cover - parser guards this
raise AssertionError(f"unsupported workload: {args.workload}")

profile_requested = args.profile_output is not None or (
profile_requested = args.profile_label is not None or (
args.loop == "rsloop" and env_flag(RSLOOP_PROFILE_ENV)
)
loop_factory_for(args.loop)
baseline_rss_bytes = get_current_rss_bytes()
if profile_requested and not args.profile_output:
if profile_requested and not args.profile_label:
print(
f"[profile] Tracy enabled via {RSLOOP_PROFILE_ENV}=1 for {args.loop}/{args.workload}",
flush=True,
Expand All @@ -435,29 +427,23 @@ def child_main(args: argparse.Namespace) -> int:
if args.loop != "rsloop":
raise RuntimeError("profiling is only supported for rsloop")
rsloop = importlib.import_module("rsloop")
with rsloop.profile(args.profile_output, frequency=args.profile_frequency):
if args.profile_label:
print(f"[profile] Tracy session label: {args.profile_label}", flush=True)
with rsloop.profile():
result = run_with_loop(args.loop, coro)
else:
result = run_with_loop(args.loop, coro)
finally:
gc.enable()

result = ChildResult(
loop=result.loop,
workload=result.workload,
seconds=result.seconds,
operations=result.operations,
result = replace(
result,
baseline_rss_bytes=baseline_rss_bytes,
peak_rss_bytes=get_peak_rss_bytes(),
peak_rss_delta_bytes=0,
)
result = ChildResult(
loop=result.loop,
workload=result.workload,
seconds=result.seconds,
operations=result.operations,
baseline_rss_bytes=result.baseline_rss_bytes,
peak_rss_bytes=result.peak_rss_bytes,
result = replace(
result,
peak_rss_delta_bytes=max(0, result.peak_rss_bytes - result.baseline_rss_bytes),
)

Expand All @@ -484,7 +470,7 @@ def run_child(
workload: str,
args: argparse.Namespace,
*,
profile_output: str | None = None,
profile_label: str | None = None,
) -> ChildResult:
cmd = [
sys.executable,
Expand All @@ -504,11 +490,9 @@ def run_child(
str(args.tcp_roundtrips),
"--payload-size",
str(args.payload_size),
"--profile-frequency",
str(args.profile_frequency),
]
if profile_output is not None:
cmd.extend(["--profile-output", profile_output])
if profile_label is not None:
cmd.extend(["--profile-label", profile_label])
env = os.environ.copy()
if loop_name == "rsloop":
env["RSLOOP_USE_FAST_STREAMS"] = "1" if args.rsloop_fast_streams else "0"
Expand Down Expand Up @@ -662,16 +646,14 @@ def parent_main(args: argparse.Namespace) -> int:
print(f"Running {workload} on {loop_name}...")
if profile_rsloop_dir and loop_name == "rsloop":
os.makedirs(profile_rsloop_dir, exist_ok=True)
profile_output = os.path.join(
profile_rsloop_dir, f"rsloop-{workload}.svg"
)
print(f" starting Tracy session labeled {profile_output}")
profile_label = os.path.join(profile_rsloop_dir, f"rsloop-{workload}")
print(f" starting Tracy session labeled {profile_label}")
run_child(
script_path,
loop_name,
workload,
args,
profile_output=profile_output,
profile_label=profile_label,
)
for _ in range(args.warmups):
run_child(script_path, loop_name, workload, args)
Expand Down
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ set shell := ["bash", "-euo", "pipefail", "-c"]
tls-test-certs outdir="tests/fixtures/tls":
./scripts/generate-test-tls-certs.sh {{outdir}}

fmt:
uv run ruff format .
cargo fmt --all

test:
uv run python -m unittest discover -s tests
30 changes: 11 additions & 19 deletions python/rsloop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,32 +105,24 @@ def profiler_running() -> bool:
return __profiler_running()


def start_profiler(*, frequency: int = 999) -> None:
__start_profiler(frequency=frequency)
def start_profiler() -> None:
"""Start a Tracy profiling session."""
__start_profiler()


def stop_profiler(
path: str | __os.PathLike[str] | None = None,
*,
format: str | None = None,
) -> str | None:
resolved = None if path is None else __os.fspath(path)
return __stop_profiler(resolved, format=format)
def stop_profiler() -> None:
"""Stop the active Tracy profiling session."""
__stop_profiler()


@__contextlib.contextmanager
def profile(
path: str | __os.PathLike[str] | None = None,
*,
frequency: int = 999,
format: str | None = None,
) -> __typing.Iterator[str | None]:
resolved_path = None if path is None else __os.fspath(path)
start_profiler(frequency=frequency)
def profile() -> __typing.Iterator[None]:
"""Context manager wrapper around ``start_profiler()`` / ``stop_profiler()``."""
start_profiler()
try:
yield resolved_path
yield None
finally:
stop_profiler(resolved_path, format=format)
stop_profiler()


__ORIG_OPEN_CONNECTION = __asyncio.open_connection
Expand Down
38 changes: 9 additions & 29 deletions src/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,15 @@ mod imp {
ACTIVE_PROFILER.get_or_init(|| Mutex::new(None))
}

fn ensure_ignored_format(format: Option<&str>) -> PyResult<()> {
if let Some(format) = format {
return Err(PyValueError::new_err(format!(
"Tracy does not support file output formats; got format={format:?}"
)));
}
Ok(())
}

#[pyfunction(signature = (*, frequency = 999))]
pub fn start_profiler(frequency: i32) -> PyResult<()> {
if frequency <= 0 {
return Err(PyValueError::new_err(
"profiler frequency must be greater than zero",
));
}

#[pyfunction]
pub fn start_profiler() -> PyResult<()> {
let mut active = active_profiler()
.lock()
.map_err(|_| PyRuntimeError::new_err("profiler state mutex is poisoned"))?;
if active.is_some() {
return Err(PyRuntimeError::new_err("profiler is already running"));
}

let _ = frequency;
let client = Client::start();
client.set_thread_name("python-main");
let session_span = client.clone().span_alloc(
Expand All @@ -70,10 +54,8 @@ mod imp {
.unwrap_or(false)
}

#[pyfunction(signature = (path = None, *, format = None))]
pub fn stop_profiler(path: Option<String>, format: Option<&str>) -> PyResult<Option<String>> {
ensure_ignored_format(format)?;

#[pyfunction]
pub fn stop_profiler() -> PyResult<()> {
let active = active_profiler()
.lock()
.map_err(|_| PyRuntimeError::new_err("profiler state mutex is poisoned"))?
Expand All @@ -83,7 +65,7 @@ mod imp {
slot.borrow_mut().take();
});
drop(active.client);
Ok(path)
Ok(())
}
}

Expand All @@ -100,15 +82,13 @@ mod imp {
false
}

#[pyfunction(signature = (*, frequency = 999))]
pub fn start_profiler(frequency: i32) -> PyResult<()> {
let _ = frequency;
#[pyfunction]
pub fn start_profiler() -> PyResult<()> {
Err(PyRuntimeError::new_err(PROFILER_DISABLED_MESSAGE))
}

#[pyfunction(signature = (path = None, *, format = None))]
pub fn stop_profiler(path: Option<String>, format: Option<&str>) -> PyResult<Option<String>> {
let _ = (path, format);
#[pyfunction]
pub fn stop_profiler() -> PyResult<()> {
Err(PyRuntimeError::new_err(PROFILER_DISABLED_MESSAGE))
}
}
Expand Down