functiontrace 0.5.2__tar.gz → 0.6.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.
- {functiontrace-0.5.2 → functiontrace-0.6.0}/Cargo.toml +2 -2
- {functiontrace-0.5.2/functiontrace.egg-info → functiontrace-0.6.0}/PKG-INFO +1 -1
- {functiontrace-0.5.2 → functiontrace-0.6.0/functiontrace.egg-info}/PKG-INFO +1 -1
- {functiontrace-0.5.2 → functiontrace-0.6.0}/functiontrace.egg-info/SOURCES.txt +2 -1
- {functiontrace-0.5.2 → functiontrace-0.6.0}/pyproject.toml +1 -1
- {functiontrace-0.5.2 → functiontrace-0.6.0}/rust/lib.rs +362 -322
- functiontrace-0.6.0/rust/multiprocessing.sh.tpl +53 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/LICENSE.md +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/MANIFEST.in +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/README.rst +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/build.rs +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/functiontrace.egg-info/dependency_links.txt +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/functiontrace.egg-info/entry_points.txt +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/functiontrace.egg-info/top_level.txt +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/functiontrace.py +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/setup.cfg +0 -0
- {functiontrace-0.5.2 → functiontrace-0.6.0}/setup.py +0 -0
|
@@ -10,7 +10,7 @@ functiontrace-server = { version = "0.8.4", default-features = false, features =
|
|
|
10
10
|
jiff = { version = "0.2", default-features = false, features = ["std", "serde"] }
|
|
11
11
|
libc = "0.2"
|
|
12
12
|
owo-colors = "4.2"
|
|
13
|
-
pyo3 = { version = "0.
|
|
13
|
+
pyo3 = { version = "0.30", features = ["eyre"] }
|
|
14
14
|
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
|
|
15
15
|
rmp-serde = "1.3"
|
|
16
16
|
serde = "1"
|
|
@@ -20,7 +20,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["a
|
|
|
20
20
|
|
|
21
21
|
[build-dependencies]
|
|
22
22
|
toml = { version = "1.1", default-features = false, features = ["parse", "serde"] }
|
|
23
|
-
pyo3-build-config = { version = "0.
|
|
23
|
+
pyo3-build-config = { version = "0.30" }
|
|
24
24
|
|
|
25
25
|
[lib]
|
|
26
26
|
# See https://github.com/PyO3/pyo3 for details
|
|
@@ -6,7 +6,7 @@ mod _functiontrace_rs {
|
|
|
6
6
|
use pyo3::prelude::*;
|
|
7
7
|
use serde::{Deserialize, Serialize};
|
|
8
8
|
use std::borrow::Cow;
|
|
9
|
-
use std::cell::
|
|
9
|
+
use std::cell::RefCell;
|
|
10
10
|
use std::ffi::{CStr, OsString};
|
|
11
11
|
use std::io::Write;
|
|
12
12
|
use std::os::unix::net::UnixStream;
|
|
@@ -50,7 +50,7 @@ mod _functiontrace_rs {
|
|
|
50
50
|
/// once full.
|
|
51
51
|
fn write(&mut self, src: &[u8]) -> std::io::Result<usize> {
|
|
52
52
|
if !STARTED.load(Ordering::Relaxed) {
|
|
53
|
-
// We haven't started yet, so we shouldn't write anything out.
|
|
53
|
+
// We haven't started yet, so we shouldn't write anything out. This avoids logging
|
|
54
54
|
// various internal setup things before we're ready.
|
|
55
55
|
return Ok(src.len());
|
|
56
56
|
}
|
|
@@ -58,8 +58,8 @@ mod _functiontrace_rs {
|
|
|
58
58
|
let bytes = src.len();
|
|
59
59
|
let tail = self.head + bytes;
|
|
60
60
|
|
|
61
|
-
if tail
|
|
62
|
-
if bytes
|
|
61
|
+
if tail > self.buffer.len() {
|
|
62
|
+
if bytes > self.buffer.len() {
|
|
63
63
|
// We'll never be able to fit this much.
|
|
64
64
|
return Err(std::io::Error::new(
|
|
65
65
|
std::io::ErrorKind::StorageFull,
|
|
@@ -67,7 +67,7 @@ mod _functiontrace_rs {
|
|
|
67
67
|
));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// We can't fit any more data, so need to flush.
|
|
70
|
+
// We can't fit any more data, so need to flush. The caller should retry after
|
|
71
71
|
// this.
|
|
72
72
|
self.flush()?;
|
|
73
73
|
|
|
@@ -85,13 +85,25 @@ mod _functiontrace_rs {
|
|
|
85
85
|
|
|
86
86
|
/// Flush our local buffer to the socket.
|
|
87
87
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
88
|
-
// We're about to send all the data, so reset our buffer.
|
|
88
|
+
// We're about to send all the data, so reset our buffer. Do this now to ensure we
|
|
89
89
|
// don't miss it, since the buffer will be left in an invalid state if this doesn't
|
|
90
90
|
// happen.
|
|
91
|
-
let head = self.head;
|
|
92
|
-
self.head = 0;
|
|
91
|
+
let head = std::mem::take(&mut self.head);
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
if head != 0 {
|
|
94
|
+
self.socket.write_all(&self.buffer[..head])
|
|
95
|
+
} else {
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Drop for ThreadState {
|
|
102
|
+
// Attempt to flush the buffer on drop. This should be run whenever TLS is torn down for
|
|
103
|
+
// threads, which should allow us to collect data from short-lived threads that don't call
|
|
104
|
+
// `thread_teardown` themselves.
|
|
105
|
+
fn drop(&mut self) {
|
|
106
|
+
let _ = self.flush();
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -105,16 +117,15 @@ mod _functiontrace_rs {
|
|
|
105
117
|
/// `sys.argv` for our process, which is used when registering new threads.
|
|
106
118
|
static ARGV: OnceLock<String> = OnceLock::new();
|
|
107
119
|
|
|
108
|
-
/// True iff we've started tracing and haven't been marked as terminated.
|
|
120
|
+
/// True iff we've started tracing and haven't been marked as terminated. When this is
|
|
109
121
|
/// set, we're allowed to send messages to the profile generation server.
|
|
110
122
|
static STARTED: AtomicBool = AtomicBool::new(false);
|
|
111
123
|
|
|
112
124
|
thread_local! {
|
|
113
125
|
/// A per-thread [`ThreadState`] for writing events to.
|
|
114
126
|
//
|
|
115
|
-
// NOTE: The `const` block
|
|
116
|
-
|
|
117
|
-
static THREAD_STATE: Cell<*mut ThreadState> = const { Cell::new(std::ptr::null_mut()) };
|
|
127
|
+
// NOTE: The `const` block allows the macro to avoid lazy initialization.
|
|
128
|
+
static THREAD_STATE: RefCell<Option<Box<ThreadState>>> = const { RefCell::new(None) };
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
@@ -140,22 +151,17 @@ mod _functiontrace_rs {
|
|
|
140
151
|
Duration::new(time.tv_sec as u64, time.tv_nsec as u32)
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
///
|
|
144
|
-
///
|
|
145
|
-
///
|
|
154
|
+
/// Run the given callback under the [`ThreadState`] for the current thread.
|
|
155
|
+
/// Returns `None` if the thread is not registered or the state is currently borrowed.
|
|
156
|
+
///
|
|
157
|
+
/// NOTE: The latter should only happen if we're running Python code inside a tracing event,
|
|
158
|
+
/// which is something we don't actually want to trace (it would be tracing FunctionTrace
|
|
159
|
+
/// implementation rather than the user's code).
|
|
146
160
|
#[inline(always)]
|
|
147
|
-
pub fn thread_state() ->
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// We shouldn't be asked to reset a thread that hasn't yet been registered.
|
|
152
|
-
return None;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
Some(unsafe {
|
|
156
|
-
// SAFETY: state is a valid pointer to a ThreadState, since we always remove from TLS
|
|
157
|
-
// before freeing.
|
|
158
|
-
state.as_mut().expect("state is non-null")
|
|
161
|
+
pub fn thread_state<T>(access: impl FnOnce(&mut ThreadState) -> T) -> Option<T> {
|
|
162
|
+
THREAD_STATE.with(|state| {
|
|
163
|
+
let mut state = state.try_borrow_mut().ok()?;
|
|
164
|
+
Some(access(state.as_deref_mut()?))
|
|
159
165
|
})
|
|
160
166
|
}
|
|
161
167
|
|
|
@@ -241,6 +247,8 @@ mod _functiontrace_rs {
|
|
|
241
247
|
module: &Bound<'_, PyModule>,
|
|
242
248
|
output_directory: OsString,
|
|
243
249
|
) -> Result<()> {
|
|
250
|
+
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
|
|
251
|
+
|
|
244
252
|
// The name of the env var we use to notify subprocesses that there's a trace in progress.
|
|
245
253
|
const BREADCRUMB: &str = "FUNCTIONTRACE_LIVE";
|
|
246
254
|
|
|
@@ -286,7 +294,7 @@ mod _functiontrace_rs {
|
|
|
286
294
|
// We'll display this in the UI, so ensure it's under (the rather
|
|
287
295
|
// arbitrary) 100 chars.
|
|
288
296
|
if full_args.chars().count() > 95 {
|
|
289
|
-
full_args.
|
|
297
|
+
full_args = full_args.chars().take(95).collect();
|
|
290
298
|
full_args.push_str(" ...");
|
|
291
299
|
}
|
|
292
300
|
|
|
@@ -337,7 +345,16 @@ mod _functiontrace_rs {
|
|
|
337
345
|
e => e.wrap_err("Failed to launch functiontrace-server")?,
|
|
338
346
|
};
|
|
339
347
|
|
|
340
|
-
format!("/tmp/functiontrace-server.sock.{}", server.id())
|
|
348
|
+
let sockaddr = format!("/tmp/functiontrace-server.sock.{}", server.id());
|
|
349
|
+
|
|
350
|
+
// Also allocate a directory for storing wrappers (multiprocessing.set_executable,
|
|
351
|
+
// etc) we may need.
|
|
352
|
+
std::fs::DirBuilder::new()
|
|
353
|
+
.recursive(true)
|
|
354
|
+
.mode(0o700)
|
|
355
|
+
.create(format!("{sockaddr}.wrappers"))
|
|
356
|
+
.wrap_err("Failed to create wrapper directory")?;
|
|
357
|
+
sockaddr.into()
|
|
341
358
|
};
|
|
342
359
|
|
|
343
360
|
Python::attach(|_| {
|
|
@@ -414,24 +431,81 @@ mod _functiontrace_rs {
|
|
|
414
431
|
.wrap_err("Failed to close initialization socket")?;
|
|
415
432
|
}
|
|
416
433
|
|
|
434
|
+
// `multiprocessing` uses an absolute path to an executable to launch forkserver workers
|
|
435
|
+
// using `-c` for the actual code,
|
|
436
|
+
// Override this with our own wrapper that also sets up functiontrace.
|
|
437
|
+
Python::attach(|py| -> Result<()> {
|
|
438
|
+
let executable: String = py.import("sys")?.getattr("executable")?.extract()?;
|
|
439
|
+
let wrapper = {
|
|
440
|
+
// We'll store the wrappers next to the socket.
|
|
441
|
+
// TODO: Ideally we'd have these all in the same tmpdir, but that requires changing
|
|
442
|
+
// client and server code so would be a breaking change.
|
|
443
|
+
let mut directory = SOCKADDR
|
|
444
|
+
.get()
|
|
445
|
+
.expect("sockaddr is set during server initialization")
|
|
446
|
+
.clone();
|
|
447
|
+
directory.push(".wrappers");
|
|
448
|
+
|
|
449
|
+
std::path::PathBuf::from(directory)
|
|
450
|
+
.join("multiprocessing")
|
|
451
|
+
.join(executable.strip_prefix("/").unwrap_or(&executable))
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Create a multiprocessing wrapper for the current executable if it doesn't exist yet.
|
|
455
|
+
// TODO: This is slow. Better to just hash the executable.
|
|
456
|
+
std::fs::create_dir_all(
|
|
457
|
+
wrapper
|
|
458
|
+
.parent()
|
|
459
|
+
.expect("Wrappers are always in a subdirectory"),
|
|
460
|
+
)
|
|
461
|
+
.wrap_err("Failed to create multiprocessing wrapper directories")?;
|
|
462
|
+
|
|
463
|
+
match std::fs::OpenOptions::new()
|
|
464
|
+
.write(true)
|
|
465
|
+
.create_new(true)
|
|
466
|
+
.open(&wrapper)
|
|
467
|
+
{
|
|
468
|
+
Ok(mut file) => {
|
|
469
|
+
let script = include_str!("multiprocessing.sh.tpl").replace(
|
|
470
|
+
"@PYTHON_INTERPRETER@",
|
|
471
|
+
// NOTE: This is equivalent to shlex.quote
|
|
472
|
+
&format!("'{}'", executable.replace('\'', "'\"'\"'")),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// TODO: this needs to be atomic, since multiple children could hit this case at
|
|
476
|
+
// once.
|
|
477
|
+
file.write_all(script.as_bytes())
|
|
478
|
+
.wrap_err("Failed to write multiprocessing wrapper")?;
|
|
479
|
+
file.set_permissions(std::fs::Permissions::from_mode(0o700))
|
|
480
|
+
.wrap_err("Failed to `chmod +x` multiprocessing wrapper")?;
|
|
481
|
+
}
|
|
482
|
+
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {}
|
|
483
|
+
Err(err) => return Err(err).wrap_err("Failed to create multiprocessing wrapper"),
|
|
484
|
+
}
|
|
485
|
+
py.import("multiprocessing")?
|
|
486
|
+
.getattr("set_executable")?
|
|
487
|
+
.call1((wrapper,))?;
|
|
488
|
+
Ok(())
|
|
489
|
+
})
|
|
490
|
+
.wrap_err("Failed to configure multiprocessing tracing")?;
|
|
491
|
+
|
|
417
492
|
////////////////////////////////////////////////////////////////////////////
|
|
418
493
|
// Teardown Configuration
|
|
419
494
|
////////////////////////////////////////////////////////////////////////////
|
|
420
495
|
// Ensure we're properly handling teardown scenarios, including flushing data in both
|
|
421
496
|
// single and multi-threaded scenarios, and reseting anything that's needed on forks.
|
|
422
497
|
|
|
423
|
-
// Mark that we'll need to forget some information on forks.
|
|
424
|
-
//
|
|
425
|
-
// information.
|
|
498
|
+
// Mark that we'll need to forget some information on forks. In particular, we shouldn't
|
|
499
|
+
// think that we have a thread that's sent any information.
|
|
426
500
|
if unsafe { libc::pthread_atfork(None, None, Some(c_thread_reset)) } != 0 {
|
|
427
501
|
return Err(eyre!("Failed to register pthread_atfork() handler"));
|
|
428
502
|
}
|
|
429
503
|
|
|
430
504
|
// Things can get into a weird state during shutdown due to GC
|
|
431
|
-
// (bugs.python.org/issue21512).
|
|
432
|
-
//
|
|
505
|
+
// (bugs.python.org/issue21512). Halt our tracing instead to avoid odd behavior and ensure
|
|
506
|
+
// we've flushed all our data.
|
|
433
507
|
//
|
|
434
|
-
// NOTE: We specifically do this rather than `Py_AtExit`
|
|
508
|
+
// NOTE: We specifically do this rather than `Py_AtExit` since we want to run before all
|
|
435
509
|
// the Python teardown occurs.
|
|
436
510
|
Python::attach(|py| -> Result<()> {
|
|
437
511
|
py.import("atexit")
|
|
@@ -496,7 +570,8 @@ mod _functiontrace_rs {
|
|
|
496
570
|
_: &Bound<'_, pyo3::types::PyTuple>,
|
|
497
571
|
_kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
|
|
498
572
|
) -> Result<()> {
|
|
499
|
-
// Stop profiling and don't allow further logging
|
|
573
|
+
// Stop profiling and don't allow further logging.
|
|
574
|
+
// The outstanding messages will be flushed on exit when the ThreadState is dropped.
|
|
500
575
|
STARTED.store(false, Ordering::Relaxed);
|
|
501
576
|
|
|
502
577
|
unsafe {
|
|
@@ -504,23 +579,19 @@ mod _functiontrace_rs {
|
|
|
504
579
|
pyo3::ffi::PyEval_SetProfile(None, std::ptr::null_mut());
|
|
505
580
|
}
|
|
506
581
|
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
thread_teardown(writer).wrap_err("Failed to teardown main thread")?;
|
|
510
|
-
}
|
|
582
|
+
// TLS destruction isn't guaranteed to run on the main thread's shutdown.
|
|
583
|
+
thread_teardown();
|
|
511
584
|
|
|
512
585
|
Ok(())
|
|
513
586
|
}
|
|
514
587
|
|
|
515
588
|
/// Register a new thread, including creating the UnixStream to log messages for this thread
|
|
516
|
-
/// on.
|
|
517
|
-
///
|
|
518
|
-
/// A new [`ThreadState`] will be returned, which must freed once the thread has terminated.
|
|
589
|
+
/// on. The new [`ThreadState`] is stored in TLS until until [`thread_teardown`] is run.
|
|
519
590
|
fn thread_register() -> Result<()> {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
591
|
+
assert!(
|
|
592
|
+
THREAD_STATE.with(|state| { state.borrow().is_none() }),
|
|
593
|
+
"We can't have a ThreadState yet since we're responsible for registering it"
|
|
594
|
+
);
|
|
524
595
|
|
|
525
596
|
let register =
|
|
526
597
|
function_trace::FunctionTrace::RegisterThread(function_trace::ThreadRegistration {
|
|
@@ -544,37 +615,28 @@ mod _functiontrace_rs {
|
|
|
544
615
|
.wrap_err("Failed to emit RegisterThread message")?;
|
|
545
616
|
|
|
546
617
|
// Initialize the `ThreadState` for this thread, then associate the socket with it.
|
|
547
|
-
|
|
548
|
-
// We'll move this state into thread local storage, and will need to recover it on
|
|
549
|
-
// termination in order to free it.
|
|
550
|
-
let state = Box::into_raw(Box::new(ThreadState {
|
|
618
|
+
let state = Box::new(ThreadState {
|
|
551
619
|
socket,
|
|
552
620
|
|
|
553
621
|
buffer: [0u8; MSGPACK_BUFFER],
|
|
554
622
|
head: 0,
|
|
555
|
-
})
|
|
623
|
+
});
|
|
556
624
|
|
|
557
625
|
// Store our state in thread-specific storage so we can find it in the future.
|
|
558
626
|
// Tracing begins for the thread at this point, since we've now published `state` for other
|
|
559
627
|
// functions to start using.
|
|
560
|
-
THREAD_STATE.set(state);
|
|
628
|
+
THREAD_STATE.set(Some(state));
|
|
561
629
|
|
|
562
630
|
Ok(())
|
|
563
631
|
}
|
|
564
632
|
|
|
565
|
-
/// Tear down the given thread by
|
|
633
|
+
/// Tear down the given thread by dropping its [`ThreadState`], which will also flush any
|
|
634
|
+
/// outstanding messages.
|
|
566
635
|
///
|
|
567
|
-
/// This is called when the thread has shut down
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
.wrap_err("Failed to flush remaining messages")?;
|
|
572
|
-
|
|
573
|
-
// TODO: We should tear down the socket to avoid leaking O(# threads) resources.
|
|
574
|
-
// Historically doing this has crashed for some reason...
|
|
575
|
-
//
|
|
576
|
-
// TODO: And we should probably free state since it's pretty large...
|
|
577
|
-
Ok(())
|
|
636
|
+
/// This is called when the thread has shut down but we can't rely on `ThreadState::drop`
|
|
637
|
+
/// running, including in multiprocess situations.
|
|
638
|
+
pub fn thread_teardown() {
|
|
639
|
+
let _ = THREAD_STATE.take();
|
|
578
640
|
}
|
|
579
641
|
|
|
580
642
|
#[unsafe(no_mangle)]
|
|
@@ -588,26 +650,21 @@ mod _functiontrace_rs {
|
|
|
588
650
|
/// This is useful when we've just forked and want to ensure we don't reuse an existing socket.
|
|
589
651
|
fn thread_reset() -> Result<()> {
|
|
590
652
|
if !STARTED.load(Ordering::Relaxed) {
|
|
591
|
-
// We haven't actually started yet, but are for some reason being asked
|
|
592
|
-
//
|
|
653
|
+
// We haven't actually started yet, but are for some reason being asked to fork. This
|
|
654
|
+
// appears to be OS dependent.
|
|
593
655
|
return Ok(());
|
|
594
656
|
}
|
|
595
657
|
|
|
596
|
-
//
|
|
597
|
-
let state =
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
658
|
+
// Remove the old state without flushing the parent's socket.
|
|
659
|
+
let mut state = THREAD_STATE
|
|
660
|
+
.take()
|
|
661
|
+
.ok_or_eyre(eyre!("Thread wasn't yet registered"))?;
|
|
662
|
+
// Empty the buffer so the `Drop` impl doesn't flush it.
|
|
663
|
+
state.head = 0;
|
|
664
|
+
drop(state);
|
|
601
665
|
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
// SAFETY: `state` came from `Box::into_raw` previously and can only be freed once.
|
|
605
|
-
let _ = Box::from_raw(state);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Create a new thread for this process. We don't need to start an entire new trace like
|
|
609
|
-
// with subprocess calls since we were forked and therefore already share our
|
|
610
|
-
// configuration.
|
|
666
|
+
// Create a new thread for this process. We don't need to start an entire new trace like
|
|
667
|
+
// with subprocess calls since we were forked and therefore already share our configuration.
|
|
611
668
|
thread_register().wrap_err("Failed to register new thread")?;
|
|
612
669
|
|
|
613
670
|
Ok(())
|
|
@@ -700,7 +757,7 @@ mod _functiontrace_rs {
|
|
|
700
757
|
Ok(())
|
|
701
758
|
}
|
|
702
759
|
|
|
703
|
-
/// The core logging function for FunctionTrace.
|
|
760
|
+
/// The core logging function for FunctionTrace. Configured by `PyEval_SetProfile`, and used
|
|
704
761
|
/// to record all function calls/returns.
|
|
705
762
|
extern "C" fn functiontrace(
|
|
706
763
|
_obj: *mut pyo3::ffi::PyObject,
|
|
@@ -708,142 +765,141 @@ mod _functiontrace_rs {
|
|
|
708
765
|
what: i32,
|
|
709
766
|
arg: *mut pyo3::ffi::PyObject,
|
|
710
767
|
) -> i32 {
|
|
711
|
-
let Some(mut writer) = thread_state() else {
|
|
712
|
-
// No reason to look at the event since we can't log it yet.
|
|
713
|
-
return 0;
|
|
714
|
-
};
|
|
715
|
-
|
|
716
768
|
if !STARTED.load(Ordering::Relaxed) {
|
|
717
769
|
// We aren't running (either starting up or shutting down), so shouldn't write
|
|
718
770
|
// anything.
|
|
719
771
|
return 0;
|
|
720
772
|
}
|
|
721
773
|
|
|
722
|
-
|
|
774
|
+
// We hold a mutable reference to the current ThreadState throughout this entire function.
|
|
775
|
+
// This ensures that we don't log anything from Python code that we call as part of the
|
|
776
|
+
// function (such as a `str()` implementation), since that's not part of the user's code.
|
|
777
|
+
thread_state(|mut writer| {
|
|
778
|
+
let time = trace_time();
|
|
723
779
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
780
|
+
// Return the function name when we're in a C_* context.
|
|
781
|
+
let c_func_name = || {
|
|
782
|
+
let func = arg as *mut pyo3::ffi::PyCFunctionObject;
|
|
727
783
|
|
|
728
|
-
unsafe {
|
|
729
|
-
// SAFETY: In the `C_*` cases, `arg` is a non-NULL `*PyCFunctionObject`, which
|
|
730
|
-
// always contains a valid method.
|
|
731
|
-
let method = (*func).m_ml;
|
|
732
|
-
(*method).ml_name
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
// Given a C string sourced from Python, convert it to a Cow that can be serialized.
|
|
736
|
-
let cstr_to_cow = |ptr: *const i8| {
|
|
737
|
-
if !ptr.is_null() {
|
|
738
784
|
unsafe {
|
|
739
|
-
// SAFETY: `
|
|
740
|
-
//
|
|
741
|
-
|
|
785
|
+
// SAFETY: In the `C_*` cases, `arg` is a non-NULL `*PyCFunctionObject`, which
|
|
786
|
+
// always contains a valid method.
|
|
787
|
+
let method = (*func).m_ml;
|
|
788
|
+
(*method).ml_name
|
|
742
789
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
Python::attach(|py| {
|
|
790
|
+
};
|
|
791
|
+
// Given a C string sourced from Python, convert it to a Cow that can be serialized.
|
|
792
|
+
let cstr_to_cow = |ptr: *const i8| {
|
|
793
|
+
if !ptr.is_null() {
|
|
794
|
+
unsafe {
|
|
795
|
+
// SAFETY: `ptr` is non-NULL and contains a valid C string.
|
|
796
|
+
// TODO: Can this change underneath us?
|
|
797
|
+
CStr::from_ptr(ptr)
|
|
798
|
+
}
|
|
799
|
+
.to_str()
|
|
800
|
+
.unwrap_or("<invalid string>")
|
|
801
|
+
.into()
|
|
802
|
+
} else {
|
|
803
|
+
Cow::Borrowed("<null>")
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
match what {
|
|
807
|
+
pyo3::ffi::PyTrace_CALL | pyo3::ffi::PyTrace_RETURN => {
|
|
808
|
+
// Cache information about each frame we see so that we don't need to look it up
|
|
809
|
+
// every time, which involves expensive Python operations.
|
|
810
|
+
static FRAMES: LazyLock<
|
|
811
|
+
dashmap::DashMap<usize, (Cow<'static, str>, Cow<'static, str>)>,
|
|
812
|
+
> = LazyLock::new(|| {
|
|
813
|
+
dashmap::DashMap::with_capacity(
|
|
814
|
+
// Even small programs will have a decent amount of function calls in the
|
|
815
|
+
// interpreter setup.
|
|
816
|
+
512,
|
|
817
|
+
)
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
let entry = FRAMES.entry(frame.addr()).or_insert_with(|| {
|
|
821
|
+
// We've never seen this frame before, so we'll need to load its information.
|
|
822
|
+
// This is fairly slow, since it'll involve going via Python rather than fast
|
|
823
|
+
// struct accesses.
|
|
779
824
|
let code = unsafe {
|
|
780
|
-
// SAFETY:
|
|
781
|
-
// came from `PyFrame_GetCode`.
|
|
782
|
-
Bound::from_borrowed_ptr(py, code)
|
|
783
|
-
};
|
|
825
|
+
// SAFETY: frame is guaranteed to be a valid PyFrameObject
|
|
784
826
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if let Ok(filename) = obj.cast::<pyo3::types::PyString>() {
|
|
791
|
-
Cow::from(filename.to_string_lossy().into_owned())
|
|
792
|
-
} else {
|
|
793
|
-
Cow::Borrowed("<UNKNOWN>")
|
|
794
|
-
}
|
|
795
|
-
})
|
|
796
|
-
.unwrap_or_else(|_| Cow::Borrowed("<NONE>"))
|
|
827
|
+
// We'll be storing `frame` in our hashmap, so we don't want anyone
|
|
828
|
+
// else freeing it.
|
|
829
|
+
pyo3::ffi::Py_IncRef(frame as *mut pyo3::ffi::PyObject);
|
|
830
|
+
|
|
831
|
+
pyo3::ffi::PyFrame_GetCode(frame) as *mut pyo3::ffi::PyObject
|
|
797
832
|
};
|
|
798
833
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
pyo3::intern!(py, "co_name")
|
|
806
|
-
}));
|
|
834
|
+
Python::attach(|py| {
|
|
835
|
+
let code = unsafe {
|
|
836
|
+
// SAFETY: code is guaranteed to be a valid PyCodeObject since it
|
|
837
|
+
// came from `PyFrame_GetCode`.
|
|
838
|
+
Bound::from_owned_ptr(py, code)
|
|
839
|
+
};
|
|
807
840
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
841
|
+
// Given a Python object that _should_ be a string, convert it into an
|
|
842
|
+
// a Rust string.
|
|
843
|
+
let unicode_converter = |pyobj: PyResult<Bound<'_, PyAny>>| {
|
|
844
|
+
pyobj
|
|
845
|
+
.map(|obj| {
|
|
846
|
+
if let Ok(filename) = obj.cast::<pyo3::types::PyString>() {
|
|
847
|
+
Cow::from(filename.to_string_lossy().into_owned())
|
|
848
|
+
} else {
|
|
849
|
+
Cow::Borrowed("<UNKNOWN>")
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
.unwrap_or_else(|_| Cow::Borrowed("<NONE>"))
|
|
853
|
+
};
|
|
811
854
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
855
|
+
let filename =
|
|
856
|
+
unicode_converter(code.getattr(pyo3::intern!(py, "co_filename")));
|
|
857
|
+
let func_name = unicode_converter(code.getattr(if cfg!(Py_3_11) {
|
|
858
|
+
// co_qualname is better but was only added in 3.11.
|
|
859
|
+
pyo3::intern!(py, "co_qualname")
|
|
860
|
+
} else {
|
|
861
|
+
pyo3::intern!(py, "co_name")
|
|
862
|
+
}));
|
|
816
863
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
864
|
+
(filename, func_name)
|
|
865
|
+
})
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
if what == pyo3::ffi::PyTrace_CALL {
|
|
869
|
+
let linenumber = unsafe { pyo3::ffi::PyFrame_GetLineNumber(frame) }
|
|
870
|
+
.try_into()
|
|
871
|
+
.expect("Invalid line number for executing frame");
|
|
872
|
+
|
|
873
|
+
function_trace::FunctionTrace::Call {
|
|
874
|
+
filename: entry.0.clone(),
|
|
875
|
+
func_name: entry.1.clone(),
|
|
876
|
+
linenumber,
|
|
877
|
+
time,
|
|
878
|
+
}
|
|
879
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
880
|
+
.expect("Failed to emit Call message");
|
|
881
|
+
} else {
|
|
882
|
+
function_trace::FunctionTrace::Return {
|
|
883
|
+
func_name: entry.1.clone(),
|
|
884
|
+
time,
|
|
885
|
+
}
|
|
886
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
887
|
+
.expect("Failed to emit Return message")
|
|
829
888
|
}
|
|
830
|
-
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
831
|
-
.expect("Failed to emit Return message")
|
|
832
889
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
};
|
|
890
|
+
pyo3::ffi::PyTrace_C_CALL => {
|
|
891
|
+
let func = unsafe {
|
|
892
|
+
// SAFETY: In the `C_CALL` case, `arg` is guaranteed to be a non-NULL pointer
|
|
893
|
+
// to a `PyCFunctionObject`.
|
|
894
|
+
&*(arg as *mut pyo3::ffi::PyCFunctionObject)
|
|
895
|
+
};
|
|
840
896
|
|
|
841
|
-
|
|
897
|
+
let func_name = cstr_to_cow(c_func_name());
|
|
842
898
|
|
|
843
|
-
|
|
844
|
-
|
|
899
|
+
// We either belong to a module or are a method on a class.
|
|
900
|
+
if !func.m_module.is_null() {
|
|
845
901
|
// NOTE: _Typically_ `m_module` is a PyModule, but there are no requirements on
|
|
846
|
-
// what type it needs to be!
|
|
902
|
+
// what type it needs to be! In practice, we only see PyModules and PyStrings.
|
|
847
903
|
Python::attach(|py| {
|
|
848
904
|
let m_module = unsafe {
|
|
849
905
|
// SAFETY: m_module is a non-NULL pointer to a valid Python object.
|
|
@@ -907,35 +963,35 @@ mod _functiontrace_rs {
|
|
|
907
963
|
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
908
964
|
}
|
|
909
965
|
.expect("Failed to emit NativeCall message");
|
|
910
|
-
}
|
|
911
|
-
pyo3::ffi::PyTrace_C_RETURN | pyo3::ffi::PyTrace_C_EXCEPTION => {
|
|
912
|
-
if what == pyo3::ffi::PyTrace_C_EXCEPTION {
|
|
913
|
-
// C exceptions aren't exposed in a particularly interesting fashion, since they're
|
|
914
|
-
// done using the `PyErr_*` functions plus a NULL return value.
|
|
915
|
-
// However, if a `C_EXCEPTION` event is sent, we won't receive the corresponding
|
|
916
|
-
// `C_RETURN` event, so in practice we treat both of these events the same.
|
|
917
|
-
//
|
|
918
|
-
// TODO: Should we try exposing information about them? We'd probably need to
|
|
919
|
-
// change the `Exception` format in `functiontrace-server`.
|
|
920
966
|
}
|
|
967
|
+
pyo3::ffi::PyTrace_C_RETURN | pyo3::ffi::PyTrace_C_EXCEPTION => {
|
|
968
|
+
if what == pyo3::ffi::PyTrace_C_EXCEPTION {
|
|
969
|
+
// C exceptions aren't exposed in a particularly interesting fashion, since they're
|
|
970
|
+
// done using the `PyErr_*` functions plus a NULL return value.
|
|
971
|
+
// However, if a `C_EXCEPTION` event is sent, we won't receive the corresponding
|
|
972
|
+
// `C_RETURN` event, so in practice we treat both of these events the same.
|
|
973
|
+
//
|
|
974
|
+
// TODO: Should we try exposing information about them? We'd probably need to
|
|
975
|
+
// change the `Exception` format in `functiontrace-server`.
|
|
976
|
+
}
|
|
921
977
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
978
|
+
function_trace::FunctionTrace::NativeReturn {
|
|
979
|
+
func_name: cstr_to_cow(c_func_name()),
|
|
980
|
+
time,
|
|
981
|
+
}
|
|
982
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
983
|
+
.expect("Failed to emit NativeReturn message");
|
|
925
984
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
|
|
985
|
+
pyo3::ffi::PyTrace_EXCEPTION => {
|
|
986
|
+
// TODO: Actually handle exceptions and do something interesting.
|
|
987
|
+
// We unfortunately can't get these from `PyEval_SetProfile`, so we'd need to use a
|
|
988
|
+
// different method to capture them.
|
|
989
|
+
}
|
|
990
|
+
v => {
|
|
991
|
+
tracing::error!(event = v, "Unexpected PyTrace message!");
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
});
|
|
939
995
|
0
|
|
940
996
|
}
|
|
941
997
|
|
|
@@ -985,15 +1041,17 @@ mod _functiontrace_rs {
|
|
|
985
1041
|
}
|
|
986
1042
|
};
|
|
987
1043
|
|
|
988
|
-
let config = if let Ok(
|
|
989
|
-
.wrap_err("Failed to read config file")
|
|
990
|
-
.and_then(|x| toml::from_slice::<Config>(&x).wrap_err("Failed to parse config file"))
|
|
991
|
-
{
|
|
1044
|
+
let config = if let Ok(contents) = std::fs::read(&config_path) {
|
|
992
1045
|
// We successfully loaded an existing config file.
|
|
993
|
-
|
|
1046
|
+
toml::from_slice::<Config>(&contents).wrap_err_with(|| {
|
|
1047
|
+
format!(
|
|
1048
|
+
"Failed to load existing Functiontrace config file at {}",
|
|
1049
|
+
config_path.display()
|
|
1050
|
+
)
|
|
1051
|
+
})?
|
|
994
1052
|
} else {
|
|
995
|
-
// There's no config file, so assume this is the first time FunctionTrace has been
|
|
996
|
-
//
|
|
1053
|
+
// There's no config file, so assume this is the first time FunctionTrace has been used
|
|
1054
|
+
// and generate a new one.
|
|
997
1055
|
let logo = "\
|
|
998
1056
|
███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗████████╗██████╗ █████╗ ██████╗███████╗\n\
|
|
999
1057
|
██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝\n\
|
|
@@ -1031,7 +1089,7 @@ mod _functiontrace_rs {
|
|
|
1031
1089
|
println!();
|
|
1032
1090
|
println!(
|
|
1033
1091
|
"{}",
|
|
1034
|
-
"Welcome to FunctionTrace, a low
|
|
1092
|
+
"Welcome to FunctionTrace, a low overhead tracing profiler for Python".bold()
|
|
1035
1093
|
);
|
|
1036
1094
|
println!();
|
|
1037
1095
|
println!(
|
|
@@ -1074,8 +1132,8 @@ mod _functiontrace_rs {
|
|
|
1074
1132
|
config
|
|
1075
1133
|
};
|
|
1076
1134
|
|
|
1077
|
-
// We need to write out the config if it's changed.
|
|
1078
|
-
//
|
|
1135
|
+
// We need to write out the config if it's changed. Track the new and old configs so we can
|
|
1136
|
+
// tell.
|
|
1079
1137
|
let mut new_config = config.clone();
|
|
1080
1138
|
|
|
1081
1139
|
////////////////////////////////////////////////////////////////////////
|
|
@@ -1226,7 +1284,7 @@ mod _functiontrace_rs {
|
|
|
1226
1284
|
if let Ok(valid_expiration) = Timestamp::strptime(DEMO_KEY_FORMAT, demo_key) {
|
|
1227
1285
|
if now
|
|
1228
1286
|
.until(valid_expiration)
|
|
1229
|
-
.
|
|
1287
|
+
.is_ok_and(|remaining| remaining.is_positive())
|
|
1230
1288
|
{
|
|
1231
1289
|
// The license is still valid
|
|
1232
1290
|
eprintln!(
|
|
@@ -1387,14 +1445,14 @@ mod allocation_wrappers {
|
|
|
1387
1445
|
|
|
1388
1446
|
/// Log the given allocation details.
|
|
1389
1447
|
fn allocation_log<F: FnOnce() -> function_trace::AllocationDetails>(msg: F) {
|
|
1390
|
-
|
|
1448
|
+
crate::_functiontrace_rs::thread_state(|mut writer| {
|
|
1391
1449
|
function_trace::FunctionTrace::Allocation {
|
|
1392
1450
|
time: crate::_functiontrace_rs::trace_time(),
|
|
1393
1451
|
details: msg(),
|
|
1394
1452
|
}
|
|
1395
1453
|
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
1396
1454
|
.expect("Failed to emit Allocation message");
|
|
1397
|
-
}
|
|
1455
|
+
});
|
|
1398
1456
|
}
|
|
1399
1457
|
|
|
1400
1458
|
/// Given an allocator->ctx for a logging allocator, convert it to point to the original
|
|
@@ -1411,33 +1469,36 @@ mod allocation_wrappers {
|
|
|
1411
1469
|
#[unsafe(no_mangle)]
|
|
1412
1470
|
pub extern "C" fn log_malloc(ctx: *mut c_void, bytes: usize) -> *mut c_void {
|
|
1413
1471
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1414
|
-
let addr = wrapped_allocator
|
|
1472
|
+
if let Some(addr) = wrapped_allocator
|
|
1415
1473
|
.malloc
|
|
1416
1474
|
.map(|malloc| malloc(wrapped_allocator.ctx, bytes))
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1475
|
+
{
|
|
1476
|
+
allocation_log(|| function_trace::AllocationDetails::Alloc {
|
|
1477
|
+
bytes,
|
|
1478
|
+
addr: addr as usize,
|
|
1479
|
+
});
|
|
1480
|
+
addr
|
|
1481
|
+
} else {
|
|
1482
|
+
std::ptr::null_mut()
|
|
1483
|
+
}
|
|
1425
1484
|
}
|
|
1426
1485
|
|
|
1427
1486
|
#[unsafe(no_mangle)]
|
|
1428
1487
|
pub extern "C" fn log_calloc(ctx: *mut c_void, nelems: usize, elsize: usize) -> *mut c_void {
|
|
1429
1488
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1430
|
-
let addr = wrapped_allocator
|
|
1489
|
+
if let Some(addr) = wrapped_allocator
|
|
1431
1490
|
.calloc
|
|
1432
1491
|
.map(|calloc| calloc(wrapped_allocator.ctx, nelems, elsize))
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
});
|
|
1492
|
+
{
|
|
1493
|
+
allocation_log(|| function_trace::AllocationDetails::Alloc {
|
|
1494
|
+
bytes: nelems * elsize,
|
|
1495
|
+
addr: addr as usize,
|
|
1496
|
+
});
|
|
1439
1497
|
|
|
1440
|
-
|
|
1498
|
+
addr
|
|
1499
|
+
} else {
|
|
1500
|
+
std::ptr::null_mut()
|
|
1501
|
+
}
|
|
1441
1502
|
}
|
|
1442
1503
|
|
|
1443
1504
|
#[unsafe(no_mangle)]
|
|
@@ -1447,18 +1508,20 @@ mod allocation_wrappers {
|
|
|
1447
1508
|
new_size: usize,
|
|
1448
1509
|
) -> *mut c_void {
|
|
1449
1510
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1450
|
-
let addr = wrapped_allocator
|
|
1511
|
+
if let Some(addr) = wrapped_allocator
|
|
1451
1512
|
.realloc
|
|
1452
1513
|
.map(|realloc| realloc(wrapped_allocator.ctx, old_addr, new_size))
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
});
|
|
1514
|
+
{
|
|
1515
|
+
allocation_log(|| function_trace::AllocationDetails::Realloc {
|
|
1516
|
+
bytes: new_size,
|
|
1517
|
+
old_addr: old_addr as usize,
|
|
1518
|
+
new_addr: addr as usize,
|
|
1519
|
+
});
|
|
1460
1520
|
|
|
1461
|
-
|
|
1521
|
+
addr
|
|
1522
|
+
} else {
|
|
1523
|
+
std::ptr::null_mut()
|
|
1524
|
+
}
|
|
1462
1525
|
}
|
|
1463
1526
|
|
|
1464
1527
|
#[unsafe(no_mangle)]
|
|
@@ -1542,52 +1605,28 @@ mod hooks {
|
|
|
1542
1605
|
// Hooking Framework
|
|
1543
1606
|
////////////////////////////////////////////////////////////////////////////
|
|
1544
1607
|
|
|
1545
|
-
///
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
/// modify.
|
|
1555
|
-
macro_rules! hook_counter {
|
|
1556
|
-
// The accumulator has fully collected all of our hooks, so we can now emit a vec of them.
|
|
1557
|
-
( [$( ($target:literal, $wrapper:ident, $counter:expr) ),* $(,)?], $_counter:expr) => {
|
|
1558
|
-
[$( Hook {
|
|
1608
|
+
/// Generate a static slice of [`Hook`]s with info about the target functions.
|
|
1609
|
+
macro_rules! hooks {
|
|
1610
|
+
// Entry: setup a counter an accumulator array
|
|
1611
|
+
($(($target:literal, $wrapper:ident)),* $(,)?) => {
|
|
1612
|
+
hooks!(@count 0; []; $(($target, $wrapper),)*)
|
|
1613
|
+
};
|
|
1614
|
+
// Recursive case: consume one function and put info into the accumulator array.
|
|
1615
|
+
(@count $index:expr; [$($hooks:expr,)*]; ($target:literal, $wrapper:ident), $($rest:tt)*) => {
|
|
1616
|
+
hooks!(@count $index + 1; [$($hooks,)* Hook {
|
|
1559
1617
|
target: $target,
|
|
1560
|
-
wrapper: $wrapper::<{ $
|
|
1618
|
+
wrapper: $wrapper::<{ $index }>,
|
|
1561
1619
|
original: OnceLock::new(),
|
|
1562
|
-
})
|
|
1563
|
-
};
|
|
1564
|
-
// Handle the first iteration specifically
|
|
1565
|
-
( [ ], $counter:expr, $target:literal, $wrapper:ident, $($rest:tt),*) => {
|
|
1566
|
-
hook_counter!( [
|
|
1567
|
-
($target, $wrapper, $counter)
|
|
1568
|
-
], $counter +1, $($rest),*)
|
|
1569
|
-
};
|
|
1570
|
-
// Handle the last iteration specifically, since we'll switch out of the normal recursive
|
|
1571
|
-
// case and into our accumulator case.
|
|
1572
|
-
( [$($acc:tt),* $(,)?], $counter:expr, $target:literal, $wrapper:ident) => {
|
|
1573
|
-
hook_counter!( [
|
|
1574
|
-
$($acc),*, ($target, $wrapper, $counter)
|
|
1575
|
-
], $counter +1)
|
|
1620
|
+
},]; $($rest)*)
|
|
1576
1621
|
};
|
|
1577
|
-
//
|
|
1578
|
-
(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
/// Generate an array of [`Hook`]s. In particular, we need to generate unique references into
|
|
1586
|
-
/// [`HOOKS`] that each wrapper is aware of, meaning we need to use a custom push-down
|
|
1587
|
-
/// accumulator and counting macro.
|
|
1588
|
-
macro_rules! hooks {
|
|
1589
|
-
($(($target:literal, $wrapper:ident)),* $(,)?) => {
|
|
1590
|
-
hook_counter!([], 0, $($target, $wrapper),*)
|
|
1622
|
+
// Base case: we have no more functions to hook, so emit the hooks.
|
|
1623
|
+
(@count $index:expr; [$($hooks:expr,)*];) => {
|
|
1624
|
+
{
|
|
1625
|
+
// We need a static array with a fixed size to get a reference to. Generate this in
|
|
1626
|
+
// the macro so the caller doesn't need to manually update the size.
|
|
1627
|
+
static ENTRIES: [Hook; $index] = [$($hooks,)*];
|
|
1628
|
+
&ENTRIES
|
|
1629
|
+
}
|
|
1591
1630
|
};
|
|
1592
1631
|
}
|
|
1593
1632
|
|
|
@@ -1596,18 +1635,19 @@ mod hooks {
|
|
|
1596
1635
|
/// NOTE: This must be created by [`hooks`], since we actually generate new variants of the
|
|
1597
1636
|
/// wrapper function for each hook, allowing the function to determine which hook triggered it
|
|
1598
1637
|
/// at runtime.
|
|
1599
|
-
static HOOKS: [Hook
|
|
1638
|
+
static HOOKS: &[Hook] = hooks!(
|
|
1639
|
+
("builtins.__import__", logging_import),
|
|
1600
1640
|
("builtins.print", logging_print),
|
|
1641
|
+
("logging.critical", logging_print),
|
|
1601
1642
|
("logging.debug", logging_print),
|
|
1602
|
-
("logging.log", logging_print),
|
|
1603
|
-
("logging.info", logging_print),
|
|
1604
|
-
("logging.warning", logging_print),
|
|
1605
1643
|
("logging.error", logging_print),
|
|
1606
|
-
("logging.critical", logging_print),
|
|
1607
|
-
("logging.fatal", logging_print),
|
|
1608
1644
|
("logging.exception", logging_print),
|
|
1609
|
-
("
|
|
1610
|
-
("
|
|
1645
|
+
("logging.fatal", logging_print),
|
|
1646
|
+
("logging.info", logging_print),
|
|
1647
|
+
("logging.log", logging_print),
|
|
1648
|
+
("logging.warning", logging_print),
|
|
1649
|
+
("multiprocessing.util._exit_function", process_exit),
|
|
1650
|
+
("os._exit", process_exit),
|
|
1611
1651
|
);
|
|
1612
1652
|
|
|
1613
1653
|
/// Information, used during both runtime and at [`install`] time, about a specific function
|
|
@@ -1638,7 +1678,7 @@ mod hooks {
|
|
|
1638
1678
|
.expect("Wrapper functions can't be called until hooked");
|
|
1639
1679
|
|
|
1640
1680
|
let proxied = Python::attach(|py| -> Result<()> {
|
|
1641
|
-
|
|
1681
|
+
crate::_functiontrace_rs::thread_state(|mut writer| -> Result<()> {
|
|
1642
1682
|
let args = unsafe {
|
|
1643
1683
|
// SAFETY: `args` represents a valid argument tuple
|
|
1644
1684
|
Bound::from_borrowed_ptr(py, args)
|
|
@@ -1652,7 +1692,9 @@ mod hooks {
|
|
|
1652
1692
|
msg.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
1653
1693
|
.wrap_err("Failed to emit Allocation message")?;
|
|
1654
1694
|
}
|
|
1655
|
-
|
|
1695
|
+
Ok(())
|
|
1696
|
+
})
|
|
1697
|
+
.transpose()?;
|
|
1656
1698
|
|
|
1657
1699
|
Ok(())
|
|
1658
1700
|
})
|
|
@@ -1743,19 +1785,17 @@ mod hooks {
|
|
|
1743
1785
|
/// Though this is mixed in with the rest of the hooks, this is actually load-bearing for
|
|
1744
1786
|
/// general FunctionTrace functionality.
|
|
1745
1787
|
///
|
|
1746
|
-
///
|
|
1747
|
-
///
|
|
1748
|
-
|
|
1788
|
+
/// Anything that exits with `os._exit` (including `multiprocessing` by default) will
|
|
1789
|
+
/// immediately terminate the process without running cleanup/`atexit()`. Hook this to ensure we
|
|
1790
|
+
/// can flush as needed.
|
|
1791
|
+
extern "C" fn process_exit<const HOOK: usize>(
|
|
1749
1792
|
_self: *mut PyObject,
|
|
1750
1793
|
args: *mut PyObject,
|
|
1751
1794
|
kwargs: *mut PyObject,
|
|
1752
1795
|
) -> *mut PyObject {
|
|
1753
1796
|
let hook = &HOOKS[HOOK];
|
|
1754
1797
|
|
|
1755
|
-
|
|
1756
|
-
crate::_functiontrace_rs::thread_teardown(writer)
|
|
1757
|
-
.expect("Multiprocessing teardown should be successful");
|
|
1758
|
-
}
|
|
1798
|
+
crate::_functiontrace_rs::thread_teardown();
|
|
1759
1799
|
|
|
1760
1800
|
// Directly pass the command on, since we don't need to log/etc for this.
|
|
1761
1801
|
proxy(hook, |_args| None, args, kwargs)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
# Multiprocessing passes the code to execute in workers as `-c <code>`.
|
|
4
|
+
# Strip that out and run tracing setup first, maintaining the order of arguments just in case.
|
|
5
|
+
|
|
6
|
+
remaining=$#
|
|
7
|
+
while [ "$remaining" -gt 0 ]; do
|
|
8
|
+
arg=$1
|
|
9
|
+
shift
|
|
10
|
+
remaining=$((remaining - 1))
|
|
11
|
+
case "$arg" in
|
|
12
|
+
-W|-X)
|
|
13
|
+
# These arguments can consume other arguments. Skip them so we don't parse the wrong `-c`.
|
|
14
|
+
value=$1
|
|
15
|
+
shift
|
|
16
|
+
remaining=$((remaining - 1))
|
|
17
|
+
set -- "$@" "$arg" "$value"
|
|
18
|
+
;;
|
|
19
|
+
-c)
|
|
20
|
+
code=$1
|
|
21
|
+
shift
|
|
22
|
+
remaining=$((remaining - 1))
|
|
23
|
+
set -- "$@" -c '
|
|
24
|
+
exec(
|
|
25
|
+
(
|
|
26
|
+
compile(
|
|
27
|
+
__import__("sys").argv.pop(1),
|
|
28
|
+
"<string>",
|
|
29
|
+
"exec",
|
|
30
|
+
dont_inherit=True,
|
|
31
|
+
),
|
|
32
|
+
# Start tracing before exec runs the compiled code.
|
|
33
|
+
__import__("_functiontrace_rs").begin_tracing("."),
|
|
34
|
+
)[0]
|
|
35
|
+
)
|
|
36
|
+
' "$code"
|
|
37
|
+
break
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
set -- "$@" "$arg"
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# Consume the rest of the args without looking.
|
|
46
|
+
while [ "$remaining" -gt 0 ]; do
|
|
47
|
+
arg=$1
|
|
48
|
+
shift
|
|
49
|
+
remaining=$((remaining - 1))
|
|
50
|
+
set -- "$@" "$arg"
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
exec @PYTHON_INTERPRETER@ "$@"
|
|
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
|