or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cli.mdconfig.mdindex.mdrust-api.md

rust-api.mddocs/

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

```