proctrace 0.1.0__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.
- proctrace-0.1.0/.cargo/config.toml +9 -0
- proctrace-0.1.0/.github/workflows/ci.yml +66 -0
- proctrace-0.1.0/.gitignore +34 -0
- proctrace-0.1.0/CHANGELOG.md +24 -0
- proctrace-0.1.0/Cargo.lock +165 -0
- proctrace-0.1.0/Cargo.toml +22 -0
- proctrace-0.1.0/GUIDE.md +323 -0
- proctrace-0.1.0/PKG-INFO +264 -0
- proctrace-0.1.0/README.md +250 -0
- proctrace-0.1.0/guide-codesnippet/01_basic_watch.py +22 -0
- proctrace-0.1.0/guide-codesnippet/02_selective_tracking.py +37 -0
- proctrace-0.1.0/guide-codesnippet/03_probe_decorator.py +56 -0
- proctrace-0.1.0/guide-codesnippet/04_json_export.py +52 -0
- proctrace-0.1.0/guide-codesnippet/05_fd_leak_detection.py +78 -0
- proctrace-0.1.0/guide-codesnippet/06_ipc_tracing.py +89 -0
- proctrace-0.1.0/guide-codesnippet/07_signal_dump.py +53 -0
- proctrace-0.1.0/guide-codesnippet/08_cli_usage.sh +91 -0
- proctrace-0.1.0/guide-codesnippet/README.md +29 -0
- proctrace-0.1.0/proctrace/__init__.py +34 -0
- proctrace-0.1.0/proctrace/__main__.py +4 -0
- proctrace-0.1.0/proctrace/_proctrace_core.pyi +19 -0
- proctrace-0.1.0/proctrace/_types.py +101 -0
- proctrace-0.1.0/proctrace/cli.py +168 -0
- proctrace-0.1.0/proctrace/decorators.py +78 -0
- proctrace-0.1.0/proctrace/ipc.py +183 -0
- proctrace-0.1.0/proctrace/logger.py +82 -0
- proctrace-0.1.0/proctrace/pytest_plugin.py +95 -0
- proctrace-0.1.0/proctrace/snapshot.py +126 -0
- proctrace-0.1.0/proctrace/watch.py +183 -0
- proctrace-0.1.0/pyproject.toml +36 -0
- proctrace-0.1.0/src/error.rs +26 -0
- proctrace-0.1.0/src/ipc_probe.rs +178 -0
- proctrace-0.1.0/src/lib.rs +48 -0
- proctrace-0.1.0/src/resources/linux.rs +87 -0
- proctrace-0.1.0/src/resources/macos.rs +67 -0
- proctrace-0.1.0/src/resources.rs +106 -0
- proctrace-0.1.0/src/sampler.rs +65 -0
- proctrace-0.1.0/src/signal.rs +53 -0
- proctrace-0.1.0/tests/conftest.py +67 -0
- proctrace-0.1.0/tests/test_cli.py +229 -0
- proctrace-0.1.0/tests/test_decorators.py +74 -0
- proctrace-0.1.0/tests/test_integration.py +211 -0
- proctrace-0.1.0/tests/test_ipc.py +103 -0
- proctrace-0.1.0/tests/test_logger.py +161 -0
- proctrace-0.1.0/tests/test_proctrace.py +27 -0
- proctrace-0.1.0/tests/test_pytest_plugin.py +39 -0
- proctrace-0.1.0/tests/test_snapshot.py +58 -0
- proctrace-0.1.0/tests/test_watch.py +110 -0
- proctrace-0.1.0/uv.lock +714 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[build]
|
|
2
|
+
# On macOS, the linker needs this to correctly set the install name of the .dylib
|
|
3
|
+
# so Python can find it regardless of where the venv is installed.
|
|
4
|
+
# On Linux this has no effect.
|
|
5
|
+
[target.x86_64-apple-darwin]
|
|
6
|
+
rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
|
|
7
|
+
|
|
8
|
+
[target.aarch64-apple-darwin]
|
|
9
|
+
rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
CARGO_TERM_COLOR: always
|
|
11
|
+
RUST_BACKTRACE: 1
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os: [ubuntu-latest, macos-latest]
|
|
20
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
21
|
+
|
|
22
|
+
runs-on: ${{ matrix.os }}
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout
|
|
26
|
+
uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
29
|
+
uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
|
|
33
|
+
- name: Set up Rust (stable)
|
|
34
|
+
uses: dtolnay/rust-toolchain@stable
|
|
35
|
+
with:
|
|
36
|
+
components: clippy
|
|
37
|
+
|
|
38
|
+
- name: Cache Rust build
|
|
39
|
+
uses: actions/cache@v4
|
|
40
|
+
with:
|
|
41
|
+
path: |
|
|
42
|
+
~/.cargo/registry
|
|
43
|
+
~/.cargo/git
|
|
44
|
+
target/
|
|
45
|
+
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.toml') }}
|
|
46
|
+
|
|
47
|
+
- name: Install Python build tools
|
|
48
|
+
run: pip install maturin>=1.5 pytest>=8 pytest-asyncio twine
|
|
49
|
+
|
|
50
|
+
- name: Build and install (maturin develop)
|
|
51
|
+
run: maturin develop --release
|
|
52
|
+
|
|
53
|
+
- name: Install package in editable mode
|
|
54
|
+
run: pip install -e ".[dev]"
|
|
55
|
+
|
|
56
|
+
- name: Run Clippy (Rust linter)
|
|
57
|
+
run: cargo clippy -- -D warnings
|
|
58
|
+
|
|
59
|
+
- name: Run tests
|
|
60
|
+
run: pytest tests/ -v --tb=short
|
|
61
|
+
|
|
62
|
+
- name: Build release wheel
|
|
63
|
+
run: maturin build --release
|
|
64
|
+
|
|
65
|
+
- name: Check wheel
|
|
66
|
+
run: python -m twine check target/wheels/*.whl
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Rust build artifacts
|
|
2
|
+
target/
|
|
3
|
+
Cargo.lock
|
|
4
|
+
|
|
5
|
+
# Python build artifacts
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# maturin build output
|
|
14
|
+
*.so
|
|
15
|
+
*.pyd
|
|
16
|
+
*.dylib
|
|
17
|
+
|
|
18
|
+
# Virtual environments
|
|
19
|
+
.venv/
|
|
20
|
+
venv/
|
|
21
|
+
env/
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
/research-doc
|
|
28
|
+
|
|
29
|
+
# proctrace runtime files
|
|
30
|
+
proctrace.log
|
|
31
|
+
/tmp/proctrace-*.pipe
|
|
32
|
+
|
|
33
|
+
# macOS
|
|
34
|
+
.DS_Store
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] — 2026-08-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `proctrace.watch()` context manager: monitor memory, fd, thread, child process deltas
|
|
7
|
+
- `@proctrace.probe()` decorator: wraps sync and async functions
|
|
8
|
+
- `proctrace.install_signal_handler()`: SIGUSR1-triggered thread + asyncio dump
|
|
9
|
+
- `proctrace.trace_ipc(queue)`: latency and depth tracking for queues
|
|
10
|
+
- `proctrace.trace_pipe(r, w)`: latency tracking for pipe pairs
|
|
11
|
+
- `proctrace.trace_socket(sock)`: send latency + recv buffer utilization for sockets
|
|
12
|
+
- `proctrace.ipc_report()`: human-readable IPC channel summary
|
|
13
|
+
- `ProctraceLogger`: JSONL structured logging for ResourceDelta events
|
|
14
|
+
- `pytest-proctrace` plugin: per-test resource tracking via `--proctrace`
|
|
15
|
+
- `proctrace` CLI: `run`, `dump`, `watch` subcommands
|
|
16
|
+
- Linux backend: `/proc/self/status`, `/proc/self/fd`
|
|
17
|
+
- macOS backend: `proc_pidinfo(PROC_PIDTASKINFO)`, `fcntl(F_GETPATH)`
|
|
18
|
+
- Background Rust sampler thread for peak RSS tracking
|
|
19
|
+
- Zero runtime Python dependencies
|
|
20
|
+
|
|
21
|
+
### Platform support
|
|
22
|
+
- Linux: ✓ (primary development platform)
|
|
23
|
+
- macOS: ✓ (tested on arm64 and x86_64)
|
|
24
|
+
- Windows: ✗ (no `/proc`, no `POSIX signals`)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "heck"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "libc"
|
|
13
|
+
version = "0.2.189"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "once_cell"
|
|
19
|
+
version = "1.21.4"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "portable-atomic"
|
|
25
|
+
version = "1.15.0"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "proc-macro2"
|
|
31
|
+
version = "1.0.107"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
34
|
+
dependencies = [
|
|
35
|
+
"unicode-ident",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[[package]]
|
|
39
|
+
name = "pyo3"
|
|
40
|
+
version = "0.29.2"
|
|
41
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
+
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
|
43
|
+
dependencies = [
|
|
44
|
+
"libc",
|
|
45
|
+
"once_cell",
|
|
46
|
+
"portable-atomic",
|
|
47
|
+
"pyo3-build-config",
|
|
48
|
+
"pyo3-ffi",
|
|
49
|
+
"pyo3-macros",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[[package]]
|
|
53
|
+
name = "pyo3-build-config"
|
|
54
|
+
version = "0.29.2"
|
|
55
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
56
|
+
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
|
57
|
+
dependencies = [
|
|
58
|
+
"target-lexicon",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[[package]]
|
|
62
|
+
name = "pyo3-ffi"
|
|
63
|
+
version = "0.29.2"
|
|
64
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
65
|
+
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
|
66
|
+
dependencies = [
|
|
67
|
+
"libc",
|
|
68
|
+
"pyo3-build-config",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "pyo3-macros"
|
|
73
|
+
version = "0.29.2"
|
|
74
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
+
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
|
76
|
+
dependencies = [
|
|
77
|
+
"proc-macro2",
|
|
78
|
+
"pyo3-macros-backend",
|
|
79
|
+
"quote",
|
|
80
|
+
"syn 2.0.119",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[[package]]
|
|
84
|
+
name = "pyo3-macros-backend"
|
|
85
|
+
version = "0.29.2"
|
|
86
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
+
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
|
88
|
+
dependencies = [
|
|
89
|
+
"heck",
|
|
90
|
+
"proc-macro2",
|
|
91
|
+
"quote",
|
|
92
|
+
"syn 2.0.119",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "quote"
|
|
97
|
+
version = "1.0.47"
|
|
98
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
99
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
100
|
+
dependencies = [
|
|
101
|
+
"proc-macro2",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[[package]]
|
|
105
|
+
name = "syn"
|
|
106
|
+
version = "2.0.119"
|
|
107
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
108
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
109
|
+
dependencies = [
|
|
110
|
+
"proc-macro2",
|
|
111
|
+
"quote",
|
|
112
|
+
"unicode-ident",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
[[package]]
|
|
116
|
+
name = "syn"
|
|
117
|
+
version = "3.0.3"
|
|
118
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
119
|
+
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
|
120
|
+
dependencies = [
|
|
121
|
+
"proc-macro2",
|
|
122
|
+
"quote",
|
|
123
|
+
"unicode-ident",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
[[package]]
|
|
127
|
+
name = "tap"
|
|
128
|
+
version = "0.1.0"
|
|
129
|
+
dependencies = [
|
|
130
|
+
"libc",
|
|
131
|
+
"pyo3",
|
|
132
|
+
"thiserror",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
[[package]]
|
|
136
|
+
name = "target-lexicon"
|
|
137
|
+
version = "0.13.5"
|
|
138
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
139
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
140
|
+
|
|
141
|
+
[[package]]
|
|
142
|
+
name = "thiserror"
|
|
143
|
+
version = "2.0.20"
|
|
144
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
145
|
+
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
|
146
|
+
dependencies = [
|
|
147
|
+
"thiserror-impl",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
[[package]]
|
|
151
|
+
name = "thiserror-impl"
|
|
152
|
+
version = "2.0.20"
|
|
153
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
154
|
+
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|
155
|
+
dependencies = [
|
|
156
|
+
"proc-macro2",
|
|
157
|
+
"quote",
|
|
158
|
+
"syn 3.0.3",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
[[package]]
|
|
162
|
+
name = "unicode-ident"
|
|
163
|
+
version = "1.0.24"
|
|
164
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
165
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tap"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
authors = ["arif39x,aliarif1168@gmail.com"]
|
|
6
|
+
description = "Process introspection for humans."
|
|
7
|
+
license = "MIT"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "_proctrace_core"
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
libc = "0.2"
|
|
16
|
+
pyo3 = { version = "0.29", features = ["extension-module"] }
|
|
17
|
+
thiserror = "2"
|
|
18
|
+
|
|
19
|
+
[profile.release]
|
|
20
|
+
lto = true
|
|
21
|
+
opt-level = 3
|
|
22
|
+
codegen-units = 1
|
proctrace-0.1.0/GUIDE.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# proctrace — Developer Guide
|
|
2
|
+
|
|
3
|
+
> Non-invasive process and OS-state introspection for Python.
|
|
4
|
+
> Diagnose memory leaks, FD leaks, race conditions, and IPC bottlenecks with a single import.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Architecture](#architecture)
|
|
11
|
+
2. [How to Use proctrace](#how-to-use-proctrace)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Context Manager (watch)](#context-manager-watch)
|
|
14
|
+
- [Decorator (@probe)](#decorator-probe)
|
|
15
|
+
- [IPC Tracing](#ipc-tracing)
|
|
16
|
+
- [On-demand Stack Dumps](#on-demand-stack-dumps)
|
|
17
|
+
- [CLI](#cli)
|
|
18
|
+
3. [API Reference](#api-reference)
|
|
19
|
+
4. [Code Snippets](#code-snippets)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
proctrace is a **two-layer library**: a compiled Rust extension provides the low-level OS sampling, and a pure-Python layer offers the ergonomic API surface developers actually interact with.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
29
|
+
│ Your Python Code │
|
|
30
|
+
└─────────────────────────┬───────────────────────────────────────┘
|
|
31
|
+
│ import proctrace
|
|
32
|
+
┌─────────────────────────▼───────────────────────────────────────┐
|
|
33
|
+
│ Python Public API Layer │
|
|
34
|
+
│ │
|
|
35
|
+
│ watch() / ResourceWatcher @probe decorator │
|
|
36
|
+
│ trace_ipc / trace_pipe / trace_socket / ipc_report │
|
|
37
|
+
│ install_signal_handler() │
|
|
38
|
+
│ │
|
|
39
|
+
│ proctrace/__init__.py │
|
|
40
|
+
│ proctrace/watch.py proctrace/decorators.py │
|
|
41
|
+
│ proctrace/ipc.py proctrace/snapshot.py │
|
|
42
|
+
│ proctrace/_types.py (ResourceDelta dataclass) │
|
|
43
|
+
└─────────────────────────┬───────────────────────────────────────┘
|
|
44
|
+
│ PyO3 FFI boundary
|
|
45
|
+
┌─────────────────────────▼───────────────────────────────────────┐
|
|
46
|
+
│ Rust Core Extension │
|
|
47
|
+
│ (_proctrace_core.cpython-*.so) │
|
|
48
|
+
│ │
|
|
49
|
+
│ resources.rs — snapshot_resources(), list_open_fds() │
|
|
50
|
+
│ sampler.rs — BackgroundSampler (peak RSS thread) │
|
|
51
|
+
│ ipc_probe.rs — IpcStats, SocketStats │
|
|
52
|
+
│ signal.rs — register_signal_pipe() │
|
|
53
|
+
│ lib.rs — PyO3 module root │
|
|
54
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
55
|
+
│
|
|
56
|
+
Linux /proc
|
|
57
|
+
(macOS sysctl)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Component Breakdown
|
|
61
|
+
|
|
62
|
+
| Component | File | Responsibility |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `ResourceWatcher` | `watch.py` | Context manager; snapshots resources before/after a block; runs `BackgroundSampler` in the background |
|
|
65
|
+
| `BackgroundSampler` | `sampler.rs` | Rust thread that polls RSS on a configurable interval to capture the **peak** |
|
|
66
|
+
| `ResourceDelta` | `_types.py` | Pure-Python dataclass holding all measurements; serialises to/from dict/JSON |
|
|
67
|
+
| `@probe` decorator | `decorators.py` | Wraps sync and async functions with a `ResourceWatcher`; optionally stores result on `fn.last_probe_result` |
|
|
68
|
+
| IPC tracers | `ipc.py` | Thin wrappers around `queue.Queue`, OS pipes, and `socket` that record latency and throughput via `IpcStats`/`SocketStats` |
|
|
69
|
+
| Signal handler | `snapshot.py` | Installs a self-pipe trick via `register_signal_pipe()` (Rust); a daemon thread reads it and dumps all Python thread stacks + asyncio tasks |
|
|
70
|
+
| CLI | `cli.py` | `proctrace run / watch / dump` subcommands |
|
|
71
|
+
| Rust extension | `src/` | Compiled `.so`; exposes `snapshot_resources`, `list_open_fds`, `BackgroundSampler`, `IpcStats`, `SocketStats`, `register_signal_pipe`, `probe_version` |
|
|
72
|
+
|
|
73
|
+
### Data Flow — `watch()` block
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
__enter__
|
|
77
|
+
│
|
|
78
|
+
├─ snapshot_resources() ← Rust: reads /proc/self/status
|
|
79
|
+
├─ list_open_fds() ← Rust: reads /proc/self/fd/
|
|
80
|
+
├─ threading.enumerate() ← Python stdlib
|
|
81
|
+
├─ _count_children() ← /proc/.../children or pgrep
|
|
82
|
+
└─ BackgroundSampler.start() ← Rust thread spawned
|
|
83
|
+
|
|
84
|
+
[ your code runs ]
|
|
85
|
+
|
|
86
|
+
__exit__
|
|
87
|
+
│
|
|
88
|
+
├─ BackgroundSampler.stop() → returns peak_rss_bytes
|
|
89
|
+
├─ snapshot_resources() ← after snapshot
|
|
90
|
+
├─ list_open_fds() ← after FD list
|
|
91
|
+
├─ threading.enumerate() ← after thread list
|
|
92
|
+
└─ ResourceDelta(...) ← computed and stored on .result
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## How to Use proctrace
|
|
98
|
+
|
|
99
|
+
### Installation
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install proctrace
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Build from source (requires Rust + `maturin`):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install maturin
|
|
109
|
+
maturin develop --release
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Requirements:** Python >= 3.10 · Linux or macOS
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Context Manager (`watch`)
|
|
117
|
+
|
|
118
|
+
The simplest entry point. Wrap any block to measure its resource footprint.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import proctrace
|
|
122
|
+
|
|
123
|
+
with proctrace.watch() as probe:
|
|
124
|
+
do_work()
|
|
125
|
+
|
|
126
|
+
print(probe.result.report())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Parameters of `proctrace.watch()`:**
|
|
130
|
+
|
|
131
|
+
| Parameter | Type | Default | Description |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `memory` | `bool` | `True` | Track RSS and VMS memory |
|
|
134
|
+
| `fds` | `bool` | `True` | Track open file descriptors |
|
|
135
|
+
| `threads` | `bool` | `True` | Track thread count and names |
|
|
136
|
+
| `children` | `bool` | `True` | Track child process count |
|
|
137
|
+
| `sample_interval` | `float` | `0.1` | Background peak-RSS poll interval (seconds) |
|
|
138
|
+
|
|
139
|
+
After the `with` block, `probe.result` is a `ResourceDelta`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Decorator (`@probe`)
|
|
144
|
+
|
|
145
|
+
Attach resource tracking to any function — sync or async — without changing its call site.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from proctrace.decorators import probe
|
|
149
|
+
|
|
150
|
+
@probe(memory=True, fds=True, output="stderr")
|
|
151
|
+
def load_dataset(path: str) -> list:
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
load_dataset("data.csv")
|
|
155
|
+
|
|
156
|
+
# Access the result afterwards
|
|
157
|
+
delta = load_dataset.last_probe_result
|
|
158
|
+
print(f"RSS grew by {delta.rss_delta_mb:.2f} MB")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Parameters of `@probe()`:**
|
|
162
|
+
|
|
163
|
+
| Parameter | Type | Default | Description |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| `memory` | `bool` | `True` | Track RSS / VMS |
|
|
166
|
+
| `fds` | `bool` | `True` | Track file descriptors |
|
|
167
|
+
| `threads` | `bool` | `True` | Track threads |
|
|
168
|
+
| `children` | `bool` | `False` | Track child processes |
|
|
169
|
+
| `sample_interval` | `float` | `0.05` | Background sampler interval |
|
|
170
|
+
| `output` | `"stderr"` or `"none"` | `"stderr"` | Print report automatically |
|
|
171
|
+
| `store` | `bool` | `True` | Store result on `fn.last_probe_result` |
|
|
172
|
+
|
|
173
|
+
Works identically on `async def` functions.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### IPC Tracing
|
|
178
|
+
|
|
179
|
+
Wrap queues, pipes, and sockets to collect latency and throughput metrics.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import queue, os, socket
|
|
183
|
+
from proctrace.ipc import trace_ipc, trace_pipe, trace_socket, ipc_report
|
|
184
|
+
|
|
185
|
+
# Queues
|
|
186
|
+
q = trace_ipc(queue.Queue(), name="task-queue")
|
|
187
|
+
q.put(item)
|
|
188
|
+
item = q.get()
|
|
189
|
+
|
|
190
|
+
# OS pipes
|
|
191
|
+
r_fd, w_fd = os.pipe()
|
|
192
|
+
pipe = trace_pipe(r_fd, w_fd, name="my-pipe")
|
|
193
|
+
pipe.write(b"hello")
|
|
194
|
+
data = pipe.read(5)
|
|
195
|
+
|
|
196
|
+
# Sockets
|
|
197
|
+
raw_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
198
|
+
sock = trace_socket(raw_sock, name="worker-conn")
|
|
199
|
+
sock.sendall(b"ping")
|
|
200
|
+
resp = sock.recv(1024)
|
|
201
|
+
|
|
202
|
+
# Print aggregated report for all traced channels
|
|
203
|
+
print(ipc_report())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
All traced wrappers are transparent — they support the same interface as their underlying object.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### On-demand Stack Dumps
|
|
211
|
+
|
|
212
|
+
Install a signal handler so you can trigger a full thread + asyncio task dump at any time without stopping the process.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
import proctrace
|
|
216
|
+
|
|
217
|
+
proctrace.install_signal_handler(sig="SIGUSR1")
|
|
218
|
+
|
|
219
|
+
# From another terminal:
|
|
220
|
+
# kill -USR1 <pid>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Parameters of `install_signal_handler()`:**
|
|
224
|
+
|
|
225
|
+
| Parameter | Type | Default | Description |
|
|
226
|
+
|---|---|---|---|
|
|
227
|
+
| `sig` | `str` | `"SIGUSR1"` | Signal to listen for (`SIGUSR1`, `SIGUSR2`, `SIGALRM`) |
|
|
228
|
+
| `output` | `str` or `IO` | `"stderr"` | `"stderr"`, a file path string, or a file-like object |
|
|
229
|
+
| `include_asyncio` | `bool` | `True` | Also dump asyncio tasks |
|
|
230
|
+
| `loop` | `EventLoop or None` | `None` | Specific loop to inspect; auto-detected if `None` |
|
|
231
|
+
|
|
232
|
+
The dump includes a timestamped header, each thread's last 10 stack frames, and up to 5 frames per asyncio task.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### CLI
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
proctrace <subcommand> [options]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### `proctrace run` — measure a subprocess
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
proctrace run -- python myscript.py
|
|
246
|
+
proctrace run --json -- python myscript.py # JSON to stdout
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### `proctrace watch` — live memory polling
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
proctrace watch --pid <PID> --interval 1.0 --duration 30
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Prints a live-updating table of RSS and VMS (MB) for the target process.
|
|
256
|
+
|
|
257
|
+
#### `proctrace dump` — trigger a stack dump
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
proctrace dump --pid <PID> --signal SIGUSR1
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Sends a signal to a running proctrace-instrumented process to trigger a thread dump on its stderr.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## API Reference
|
|
268
|
+
|
|
269
|
+
### `ResourceDelta`
|
|
270
|
+
|
|
271
|
+
A dataclass produced after every measurement. All fields reflect the **delta** between entry and exit of the watched block.
|
|
272
|
+
|
|
273
|
+
| Attribute | Type | Description |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `rss_delta_bytes` | `int` | RSS change in bytes (can be negative) |
|
|
276
|
+
| `vms_delta_bytes` | `int` | Virtual memory size change |
|
|
277
|
+
| `fd_delta` | `int` | Open file descriptor count change |
|
|
278
|
+
| `peak_rss_bytes` | `int` | Highest RSS observed by the background sampler |
|
|
279
|
+
| `thread_delta` | `int` | Thread count change |
|
|
280
|
+
| `child_delta` | `int` | Child process count change |
|
|
281
|
+
| `elapsed_ns` | `int` | Wall-clock time of block in nanoseconds |
|
|
282
|
+
| `leaked_fds` | `list[str]` | FD paths open at exit but not at entry |
|
|
283
|
+
| `new_thread_names` | `list[str]` | Names of threads created during the block |
|
|
284
|
+
|
|
285
|
+
**Convenience properties:**
|
|
286
|
+
|
|
287
|
+
| Property | Returns |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `.rss_delta_mb` | `float` — RSS delta in megabytes |
|
|
290
|
+
| `.vms_delta_mb` | `float` — VMS delta in megabytes |
|
|
291
|
+
| `.peak_rss_mb` | `float` — Peak RSS in megabytes |
|
|
292
|
+
| `.elapsed_ms` | `float` — Elapsed time in milliseconds |
|
|
293
|
+
|
|
294
|
+
**Serialization:**
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
d = delta.to_dict() # -> dict[str, Any]
|
|
298
|
+
j = delta.to_json(indent=2) # -> JSON string
|
|
299
|
+
copy = ResourceDelta.from_dict(d)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Formatted report:**
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
print(delta.report()) # colour-coded when stderr is a TTY
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Code Snippets
|
|
311
|
+
|
|
312
|
+
See the [`guide-codesnippet/`](./guide-codesnippet/) folder for runnable examples:
|
|
313
|
+
|
|
314
|
+
| File | What it demonstrates |
|
|
315
|
+
|---|---|
|
|
316
|
+
| [`01_basic_watch.py`](./guide-codesnippet/01_basic_watch.py) | Minimal `watch()` context manager |
|
|
317
|
+
| [`02_selective_tracking.py`](./guide-codesnippet/02_selective_tracking.py) | Enabling only specific metrics |
|
|
318
|
+
| [`03_probe_decorator.py`](./guide-codesnippet/03_probe_decorator.py) | `@probe` on sync and async functions |
|
|
319
|
+
| [`04_json_export.py`](./guide-codesnippet/04_json_export.py) | Serialising results to JSON / dict |
|
|
320
|
+
| [`05_fd_leak_detection.py`](./guide-codesnippet/05_fd_leak_detection.py) | Detecting file descriptor leaks |
|
|
321
|
+
| [`06_ipc_tracing.py`](./guide-codesnippet/06_ipc_tracing.py) | Tracing queues, pipes, and sockets |
|
|
322
|
+
| [`07_signal_dump.py`](./guide-codesnippet/07_signal_dump.py) | On-demand thread + asyncio dump via signal |
|
|
323
|
+
| [`08_cli_usage.sh`](./guide-codesnippet/08_cli_usage.sh) | CLI `run`, `watch`, and `dump` commands |
|