0
# Rust Library API
1
2
Programmatic profiling interface for integrating py-spy functionality into Rust applications. Provides both one-shot profiling and continuous sampling capabilities with full control over configuration and output processing.
3
4
## Capabilities
5
6
### PythonSpy Struct
7
8
Main profiler object for programmatic access to Python process profiling. Handles process attachment, memory access, and stack trace extraction.
9
10
```rust { .api }
11
pub struct PythonSpy {
12
// All fields are private - access through methods only
13
}
14
15
impl PythonSpy {
16
/// Creates a new PythonSpy object for the given process ID
17
pub fn new(pid: Pid, config: &Config) -> Result<PythonSpy, anyhow::Error>;
18
19
/// Attempts to create PythonSpy with retries (useful for processes still starting)
20
pub fn retry_new(pid: Pid, config: &Config, max_retries: u64) -> Result<PythonSpy, anyhow::Error>;
21
22
/// Gets current stack traces for all threads in the Python process
23
pub fn get_stack_traces(&mut self) -> Result<Vec<StackTrace>, anyhow::Error>;
24
}
25
```
26
27
**Usage Example:**
28
29
```rust
30
use py_spy::{Config, PythonSpy, Pid};
31
use anyhow::Error;
32
33
fn analyze_python_process(pid: Pid) -> Result<(), Error> {
34
let config = Config::default();
35
let mut spy = PythonSpy::new(pid, &config)?;
36
37
let traces = spy.get_stack_traces()?;
38
for trace in traces {
39
println!("Thread {}: {} frames", trace.thread_id, trace.frames.len());
40
if trace.owns_gil {
41
println!(" (holds GIL)");
42
}
43
}
44
Ok(())
45
}
46
```
47
48
### Sampler Struct
49
50
Iterator-based continuous sampling for long-running profiling sessions. Handles timing, error recovery, and subprocess monitoring.
51
52
```rust { .api }
53
pub struct Sampler {
54
pub version: Option<Version>,
55
// Private fields...
56
}
57
58
impl Sampler {
59
/// Creates a new sampler for the given process
60
pub fn new(pid: Pid, config: &Config) -> Result<Sampler, anyhow::Error>;
61
}
62
63
impl Iterator for Sampler {
64
type Item = Sample;
65
fn next(&mut self) -> Option<Self::Item>;
66
}
67
```
68
69
**Usage Example:**
70
71
```rust
72
use py_spy::{Config, Pid};
73
use py_spy::sampler::Sampler;
74
use std::time::Duration;
75
use anyhow::Error;
76
77
fn continuous_profiling(pid: Pid) -> Result<(), Error> {
78
let mut config = Config::default();
79
config.sampling_rate = 100; // 100 Hz
80
81
let sampler = Sampler::new(pid, &config)?;
82
83
for sample in sampler.take(1000) { // Collect 1000 samples
84
if let Some(delay) = sample.late {
85
if delay > Duration::from_millis(10) {
86
println!("Warning: sampling delay of {:?}", delay);
87
}
88
}
89
90
if let Some(errors) = sample.sampling_errors {
91
for (pid, error) in errors {
92
eprintln!("Sampling error for PID {}: {}", pid, error);
93
}
94
}
95
96
// Process traces
97
for trace in sample.traces {
98
if trace.active && trace.owns_gil {
99
// Analyze active GIL-holding threads
100
analyze_trace(&trace);
101
}
102
}
103
}
104
Ok(())
105
}
106
```
107
108
### Sample Struct
109
110
Single sampling result containing stack traces and metadata from one sampling interval.
111
112
```rust { .api }
113
pub struct Sample {
114
/// Stack traces collected during this sampling interval
115
pub traces: Vec<StackTrace>,
116
/// Sampling errors that occurred (per process if using --subprocesses)
117
pub sampling_errors: Option<Vec<(Pid, anyhow::Error)>>,
118
/// Delay if sampling was late (indicates system load)
119
pub late: Option<Duration>,
120
}
121
```
122
123
### Stack Trace Analysis
124
125
Core data structures for representing and analyzing Python call stacks.
126
127
```rust { .api }
128
pub struct StackTrace {
129
/// Process ID that generated this stack trace
130
pub pid: Pid,
131
/// Python thread ID (not OS thread ID)
132
pub thread_id: u64,
133
/// Python thread name if available
134
pub thread_name: Option<String>,
135
/// Operating system thread ID
136
pub os_thread_id: Option<u64>,
137
/// Whether the thread was active (not idle/sleeping)
138
pub active: bool,
139
/// Whether the thread owns the Global Interpreter Lock
140
pub owns_gil: bool,
141
/// Stack frames from top (most recent) to bottom (oldest)
142
pub frames: Vec<Frame>,
143
/// Process information for subprocess profiling
144
pub process_info: Option<Arc<ProcessInfo>>,
145
}
146
147
impl StackTrace {
148
/// Returns human-readable thread status string
149
pub fn status_str(&self) -> &str;
150
/// Formats thread ID for display
151
pub fn format_threadid(&self) -> String;
152
}
153
```
154
155
### Frame Information
156
157
Individual function call information within a stack trace.
158
159
```rust { .api }
160
pub struct Frame {
161
/// Function or method name
162
pub name: String,
163
/// Full path to source file
164
pub filename: String,
165
/// Module or shared library name
166
pub module: Option<String>,
167
/// Shortened filename for display
168
pub short_filename: Option<String>,
169
/// Line number within the file (0 for native frames without debug info)
170
pub line: i32,
171
/// Local variables if collected
172
pub locals: Option<Vec<LocalVariable>>,
173
/// Whether this is an entry frame (Python 3.11+)
174
pub is_entry: bool,
175
/// Whether this is a shim entry (Python 3.12+)
176
pub is_shim_entry: bool,
177
}
178
```
179
180
### Local Variable Information
181
182
Variable information collected when dump_locals is enabled.
183
184
```rust { .api }
185
pub struct LocalVariable {
186
/// Variable name
187
pub name: String,
188
/// Memory address of the variable
189
pub addr: usize,
190
/// Whether this is a function argument
191
pub arg: bool,
192
/// String representation of the variable value
193
pub repr: Option<String>,
194
}
195
```
196
197
### Process Information
198
199
Process metadata for subprocess profiling scenarios.
200
201
```rust { .api }
202
pub struct ProcessInfo {
203
/// Process ID
204
pub pid: Pid,
205
/// Command line arguments
206
pub command_line: String,
207
/// Parent process information (for process trees)
208
pub parent: Option<Box<ProcessInfo>>,
209
}
210
211
impl ProcessInfo {
212
/// Converts to a Frame for flamegraph output
213
pub fn to_frame(&self) -> Frame;
214
}
215
```
216
217
## Advanced Usage Patterns
218
219
### Error Handling and Recovery
220
221
```rust
222
use py_spy::{Config, PythonSpy, Pid};
223
use std::thread;
224
use std::time::Duration;
225
use anyhow::Error;
226
227
fn robust_profiling(pid: Pid) -> Result<(), Error> {
228
let config = Config::default();
229
230
// Retry connection for processes that might be starting up
231
let mut spy = match PythonSpy::retry_new(pid, &config, 10) {
232
Ok(spy) => spy,
233
Err(e) => {
234
eprintln!("Failed to attach to process {}: {}", pid, e);
235
return Err(e.into());
236
}
237
};
238
239
// Collect samples with error handling
240
for attempt in 0..100 {
241
match spy.get_stack_traces() {
242
Ok(traces) => {
243
if traces.is_empty() {
244
println!("No active threads found (attempt {})", attempt + 1);
245
} else {
246
process_traces(traces);
247
}
248
}
249
Err(e) => {
250
eprintln!("Sampling error: {}", e);
251
// Check if process still exists
252
if spy.process.exe().is_err() {
253
println!("Process {} has exited", pid);
254
break;
255
}
256
}
257
}
258
thread::sleep(Duration::from_millis(100));
259
}
260
Ok(())
261
}
262
```
263
264
### Custom Output Processing
265
266
```rust
267
use py_spy::{Config, Frame, Pid};
268
use py_spy::sampler::Sampler;
269
use std::collections::HashMap;
270
use anyhow::Error;
271
272
fn custom_analysis(pid: Pid) -> Result<(), Error> {
273
let config = Config::default();
274
let sampler = Sampler::new(pid, &config)?;
275
276
let mut function_counts: HashMap<String, u64> = HashMap::new();
277
let mut total_samples = 0;
278
279
for sample in sampler.take(1000) {
280
total_samples += 1;
281
282
for trace in sample.traces {
283
if trace.active && !trace.frames.is_empty() {
284
let top_frame = &trace.frames[0];
285
let function_key = format!("{}:{}", top_frame.filename, top_frame.name);
286
*function_counts.entry(function_key).or_insert(0) += 1;
287
}
288
}
289
}
290
291
// Print top functions by sample count
292
let mut sorted_functions: Vec<_> = function_counts.into_iter().collect();
293
sorted_functions.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
294
295
println!("Top functions by sample count (out of {} total samples):", total_samples);
296
for (function, count) in sorted_functions.iter().take(10) {
297
let percentage = (*count as f64 / total_samples as f64) * 100.0;
298
println!(" {:.1}% - {}", percentage, function);
299
}
300
301
Ok(())
302
}
303
```
304
305
### Native Extension Profiling
306
307
```rust
308
use py_spy::{Config, LockingStrategy, PythonSpy, Pid};
309
use anyhow::Error;
310
311
fn profile_with_native_extensions(pid: Pid) -> Result<(), Error> {
312
let mut config = Config::default();
313
config.native = true; // Enable native stack traces
314
config.blocking = LockingStrategy::Lock; // Required for native profiling
315
316
let mut spy = PythonSpy::new(pid, &config)?;
317
let traces = spy.get_stack_traces()?;
318
319
for trace in traces {
320
println!("Thread {} ({} frames):", trace.thread_id, trace.frames.len());
321
for frame in &trace.frames {
322
if frame.module.is_some() {
323
println!(" [NATIVE] {} in {:?}", frame.name, frame.module);
324
} else {
325
println!(" [PYTHON] {} ({}:{})", frame.name, frame.filename, frame.line);
326
}
327
}
328
}
329
Ok(())
330
}
331
```