functiontrace 0.5.3__tar.gz → 0.6.1__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.3 → functiontrace-0.6.1}/Cargo.toml +2 -2
- {functiontrace-0.5.3/functiontrace.egg-info → functiontrace-0.6.1}/PKG-INFO +1 -1
- {functiontrace-0.5.3 → functiontrace-0.6.1/functiontrace.egg-info}/PKG-INFO +1 -1
- {functiontrace-0.5.3 → functiontrace-0.6.1}/pyproject.toml +1 -1
- {functiontrace-0.5.3 → functiontrace-0.6.1}/rust/lib.rs +342 -384
- {functiontrace-0.5.3 → functiontrace-0.6.1}/LICENSE.md +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/MANIFEST.in +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/README.rst +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/build.rs +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/functiontrace.egg-info/SOURCES.txt +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/functiontrace.egg-info/dependency_links.txt +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/functiontrace.egg-info/entry_points.txt +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/functiontrace.egg-info/top_level.txt +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/functiontrace.py +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/rust/multiprocessing.sh.tpl +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/setup.cfg +0 -0
- {functiontrace-0.5.3 → functiontrace-0.6.1}/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,9 +6,9 @@ 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
|
-
use std::io::Write;
|
|
11
|
+
use std::io::{BufWriter, Read, Write};
|
|
12
12
|
use std::os::unix::net::UnixStream;
|
|
13
13
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
14
14
|
use std::sync::{LazyLock, OnceLock};
|
|
@@ -29,69 +29,31 @@ mod _functiontrace_rs {
|
|
|
29
29
|
license_key: Option<String>,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/// The size of messagepack buffers.
|
|
32
|
+
/// The size of messagepack buffers. This is picked somewhat arbitrarily, but should be
|
|
33
33
|
/// reasonably small since we'll need one per thread, and should be large enough that we
|
|
34
34
|
/// can fit many messages in before needing to do expensive UnixStream syscalls.
|
|
35
35
|
const MSGPACK_BUFFER: usize = 1 << 17;
|
|
36
36
|
|
|
37
37
|
/// Storage specific to each thread used for buffering/transmitting data.
|
|
38
38
|
pub struct ThreadState {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// TODO: Conceptually this should just be a `BufWriter`, but in practice using that
|
|
42
|
-
// triggers 0 byte writes that cause `functiontrace-server` to close the connection.
|
|
43
|
-
// Implementing this ourselves is pretty trivial, so...
|
|
44
|
-
buffer: [u8; MSGPACK_BUFFER],
|
|
45
|
-
head: usize,
|
|
39
|
+
writer: BufWriter<UnixStream>,
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
impl Write for ThreadState {
|
|
49
|
-
///
|
|
50
|
-
/// once full.
|
|
43
|
+
/// Buffer trace data for the socket while tracing is enabled.
|
|
51
44
|
fn write(&mut self, src: &[u8]) -> std::io::Result<usize> {
|
|
52
45
|
if !STARTED.load(Ordering::Relaxed) {
|
|
53
|
-
// We haven't started yet, so we shouldn't write anything out.
|
|
46
|
+
// We haven't started yet, so we shouldn't write anything out. This avoids logging
|
|
54
47
|
// various internal setup things before we're ready.
|
|
55
48
|
return Ok(src.len());
|
|
56
49
|
}
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
let tail = self.head + bytes;
|
|
60
|
-
|
|
61
|
-
if tail >= self.buffer.len() {
|
|
62
|
-
if bytes >= self.buffer.len() {
|
|
63
|
-
// We'll never be able to fit this much.
|
|
64
|
-
return Err(std::io::Error::new(
|
|
65
|
-
std::io::ErrorKind::StorageFull,
|
|
66
|
-
"msgpack buffer too small",
|
|
67
|
-
));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// We can't fit any more data, so need to flush. The caller should retry after
|
|
71
|
-
// this.
|
|
72
|
-
self.flush()?;
|
|
73
|
-
|
|
74
|
-
return Err(std::io::Error::new(
|
|
75
|
-
std::io::ErrorKind::Interrupted,
|
|
76
|
-
"msgpack buffer is full, flushing",
|
|
77
|
-
));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
self.buffer[self.head..tail].copy_from_slice(src);
|
|
81
|
-
self.head = tail;
|
|
82
|
-
|
|
83
|
-
Ok(bytes)
|
|
51
|
+
self.writer.write(src)
|
|
84
52
|
}
|
|
85
53
|
|
|
86
54
|
/// Flush our local buffer to the socket.
|
|
87
55
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
88
|
-
|
|
89
|
-
// don't miss it, since the buffer will be left in an invalid state if this doesn't
|
|
90
|
-
// happen.
|
|
91
|
-
let head = self.head;
|
|
92
|
-
self.head = 0;
|
|
93
|
-
|
|
94
|
-
self.socket.write_all(&self.buffer[..head])
|
|
56
|
+
self.writer.flush()
|
|
95
57
|
}
|
|
96
58
|
}
|
|
97
59
|
|
|
@@ -105,16 +67,15 @@ mod _functiontrace_rs {
|
|
|
105
67
|
/// `sys.argv` for our process, which is used when registering new threads.
|
|
106
68
|
static ARGV: OnceLock<String> = OnceLock::new();
|
|
107
69
|
|
|
108
|
-
/// True iff we've started tracing and haven't been marked as terminated.
|
|
70
|
+
/// True iff we've started tracing and haven't been marked as terminated. When this is
|
|
109
71
|
/// set, we're allowed to send messages to the profile generation server.
|
|
110
72
|
static STARTED: AtomicBool = AtomicBool::new(false);
|
|
111
73
|
|
|
112
74
|
thread_local! {
|
|
113
75
|
/// A per-thread [`ThreadState`] for writing events to.
|
|
114
76
|
//
|
|
115
|
-
// NOTE: The `const` block
|
|
116
|
-
|
|
117
|
-
static THREAD_STATE: Cell<*mut ThreadState> = const { Cell::new(std::ptr::null_mut()) };
|
|
77
|
+
// NOTE: The `const` block allows the macro to avoid lazy initialization.
|
|
78
|
+
static THREAD_STATE: RefCell<Option<Box<ThreadState>>> = const { RefCell::new(None) };
|
|
118
79
|
}
|
|
119
80
|
|
|
120
81
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
@@ -140,22 +101,17 @@ mod _functiontrace_rs {
|
|
|
140
101
|
Duration::new(time.tv_sec as u64, time.tv_nsec as u32)
|
|
141
102
|
}
|
|
142
103
|
|
|
143
|
-
///
|
|
144
|
-
///
|
|
145
|
-
///
|
|
104
|
+
/// Run the given callback under the [`ThreadState`] for the current thread.
|
|
105
|
+
/// Returns `None` if the thread is not registered or the state is currently borrowed.
|
|
106
|
+
///
|
|
107
|
+
/// NOTE: The latter should only happen if we're running Python code inside a tracing event,
|
|
108
|
+
/// which is something we don't actually want to trace (it would be tracing FunctionTrace
|
|
109
|
+
/// implementation rather than the user's code).
|
|
146
110
|
#[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")
|
|
111
|
+
pub fn thread_state<T>(access: impl FnOnce(&mut ThreadState) -> T) -> Option<T> {
|
|
112
|
+
THREAD_STATE.with(|state| {
|
|
113
|
+
let mut state = state.try_borrow_mut().ok()?;
|
|
114
|
+
Some(access(state.as_deref_mut()?))
|
|
159
115
|
})
|
|
160
116
|
}
|
|
161
117
|
|
|
@@ -288,7 +244,7 @@ mod _functiontrace_rs {
|
|
|
288
244
|
// We'll display this in the UI, so ensure it's under (the rather
|
|
289
245
|
// arbitrary) 100 chars.
|
|
290
246
|
if full_args.chars().count() > 95 {
|
|
291
|
-
full_args.
|
|
247
|
+
full_args = full_args.chars().take(95).collect();
|
|
292
248
|
full_args.push_str(" ...");
|
|
293
249
|
}
|
|
294
250
|
|
|
@@ -421,8 +377,28 @@ mod _functiontrace_rs {
|
|
|
421
377
|
// Close the initialization socket to trigger `functiontrace-server` to
|
|
422
378
|
// read our message, since otherwise it'll wait forever for more data.
|
|
423
379
|
socket
|
|
424
|
-
.shutdown(std::net::Shutdown::
|
|
425
|
-
.wrap_err("Failed to
|
|
380
|
+
.shutdown(std::net::Shutdown::Write)
|
|
381
|
+
.wrap_err("Failed to finish initialization message")?;
|
|
382
|
+
|
|
383
|
+
// Modern `functiontrace-server`s send their version, allowing us to ensure
|
|
384
|
+
// protocols match.
|
|
385
|
+
let version = {
|
|
386
|
+
let mut reply = Vec::with_capacity(16);
|
|
387
|
+
socket
|
|
388
|
+
.read_to_end(&mut reply)
|
|
389
|
+
.wrap_err("Failed to read server version")?;
|
|
390
|
+
|
|
391
|
+
if reply.is_empty() {
|
|
392
|
+
// Old servers will close the socket without sending anything. We know they're
|
|
393
|
+
// older than 0.8.6, which is sufficient for us to handle.
|
|
394
|
+
"0.8.6".to_string()
|
|
395
|
+
} else {
|
|
396
|
+
rmp_serde::from_slice(&reply)
|
|
397
|
+
.wrap_err("Failed to parse server version as string")?
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
tracing::info!("Connected to functiontrace-server {version}");
|
|
426
402
|
}
|
|
427
403
|
|
|
428
404
|
// `multiprocessing` uses an absolute path to an executable to launch forkserver workers
|
|
@@ -446,6 +422,7 @@ mod _functiontrace_rs {
|
|
|
446
422
|
};
|
|
447
423
|
|
|
448
424
|
// Create a multiprocessing wrapper for the current executable if it doesn't exist yet.
|
|
425
|
+
// TODO: This is slow. Better to just hash the executable.
|
|
449
426
|
std::fs::create_dir_all(
|
|
450
427
|
wrapper
|
|
451
428
|
.parent()
|
|
@@ -465,6 +442,8 @@ mod _functiontrace_rs {
|
|
|
465
442
|
&format!("'{}'", executable.replace('\'', "'\"'\"'")),
|
|
466
443
|
);
|
|
467
444
|
|
|
445
|
+
// TODO: this needs to be atomic, since multiple children could hit this case at
|
|
446
|
+
// once.
|
|
468
447
|
file.write_all(script.as_bytes())
|
|
469
448
|
.wrap_err("Failed to write multiprocessing wrapper")?;
|
|
470
449
|
file.set_permissions(std::fs::Permissions::from_mode(0o700))
|
|
@@ -486,18 +465,17 @@ mod _functiontrace_rs {
|
|
|
486
465
|
// Ensure we're properly handling teardown scenarios, including flushing data in both
|
|
487
466
|
// single and multi-threaded scenarios, and reseting anything that's needed on forks.
|
|
488
467
|
|
|
489
|
-
// Mark that we'll need to forget some information on forks.
|
|
490
|
-
//
|
|
491
|
-
// information.
|
|
468
|
+
// Mark that we'll need to forget some information on forks. In particular, we shouldn't
|
|
469
|
+
// think that we have a thread that's sent any information.
|
|
492
470
|
if unsafe { libc::pthread_atfork(None, None, Some(c_thread_reset)) } != 0 {
|
|
493
471
|
return Err(eyre!("Failed to register pthread_atfork() handler"));
|
|
494
472
|
}
|
|
495
473
|
|
|
496
474
|
// Things can get into a weird state during shutdown due to GC
|
|
497
|
-
// (bugs.python.org/issue21512).
|
|
498
|
-
//
|
|
475
|
+
// (bugs.python.org/issue21512). Halt our tracing instead to avoid odd behavior and ensure
|
|
476
|
+
// we've flushed all our data.
|
|
499
477
|
//
|
|
500
|
-
// NOTE: We specifically do this rather than `Py_AtExit`
|
|
478
|
+
// NOTE: We specifically do this rather than `Py_AtExit` since we want to run before all
|
|
501
479
|
// the Python teardown occurs.
|
|
502
480
|
Python::attach(|py| -> Result<()> {
|
|
503
481
|
py.import("atexit")
|
|
@@ -562,7 +540,8 @@ mod _functiontrace_rs {
|
|
|
562
540
|
_: &Bound<'_, pyo3::types::PyTuple>,
|
|
563
541
|
_kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
|
|
564
542
|
) -> Result<()> {
|
|
565
|
-
// Stop profiling and don't allow further logging
|
|
543
|
+
// Stop profiling and don't allow further logging.
|
|
544
|
+
// The outstanding messages will be flushed on exit when the ThreadState is dropped.
|
|
566
545
|
STARTED.store(false, Ordering::Relaxed);
|
|
567
546
|
|
|
568
547
|
unsafe {
|
|
@@ -570,23 +549,19 @@ mod _functiontrace_rs {
|
|
|
570
549
|
pyo3::ffi::PyEval_SetProfile(None, std::ptr::null_mut());
|
|
571
550
|
}
|
|
572
551
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
thread_teardown(writer).wrap_err("Failed to teardown main thread")?;
|
|
576
|
-
}
|
|
552
|
+
// TLS destruction isn't guaranteed to run on the main thread's shutdown.
|
|
553
|
+
thread_teardown();
|
|
577
554
|
|
|
578
555
|
Ok(())
|
|
579
556
|
}
|
|
580
557
|
|
|
581
558
|
/// Register a new thread, including creating the UnixStream to log messages for this thread
|
|
582
|
-
/// on.
|
|
583
|
-
///
|
|
584
|
-
/// A new [`ThreadState`] will be returned, which must freed once the thread has terminated.
|
|
559
|
+
/// on. The new [`ThreadState`] is stored in TLS until until [`thread_teardown`] is run.
|
|
585
560
|
fn thread_register() -> Result<()> {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
561
|
+
assert!(
|
|
562
|
+
THREAD_STATE.with(|state| { state.borrow().is_none() }),
|
|
563
|
+
"We can't have a ThreadState yet since we're responsible for registering it"
|
|
564
|
+
);
|
|
590
565
|
|
|
591
566
|
let register =
|
|
592
567
|
function_trace::FunctionTrace::RegisterThread(function_trace::ThreadRegistration {
|
|
@@ -610,37 +585,25 @@ mod _functiontrace_rs {
|
|
|
610
585
|
.wrap_err("Failed to emit RegisterThread message")?;
|
|
611
586
|
|
|
612
587
|
// Initialize the `ThreadState` for this thread, then associate the socket with it.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
let state = Box::into_raw(Box::new(ThreadState {
|
|
617
|
-
socket,
|
|
618
|
-
|
|
619
|
-
buffer: [0u8; MSGPACK_BUFFER],
|
|
620
|
-
head: 0,
|
|
621
|
-
}));
|
|
588
|
+
let state = Box::new(ThreadState {
|
|
589
|
+
writer: BufWriter::with_capacity(MSGPACK_BUFFER, socket),
|
|
590
|
+
});
|
|
622
591
|
|
|
623
592
|
// Store our state in thread-specific storage so we can find it in the future.
|
|
624
593
|
// Tracing begins for the thread at this point, since we've now published `state` for other
|
|
625
594
|
// functions to start using.
|
|
626
|
-
THREAD_STATE.set(state);
|
|
595
|
+
THREAD_STATE.set(Some(state));
|
|
627
596
|
|
|
628
597
|
Ok(())
|
|
629
598
|
}
|
|
630
599
|
|
|
631
|
-
/// Tear down the given thread by
|
|
600
|
+
/// Tear down the given thread by dropping its [`ThreadState`], which will also flush any
|
|
601
|
+
/// outstanding messages.
|
|
632
602
|
///
|
|
633
|
-
/// This is called when the thread has shut down
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
.wrap_err("Failed to flush remaining messages")?;
|
|
638
|
-
|
|
639
|
-
// TODO: We should tear down the socket to avoid leaking O(# threads) resources.
|
|
640
|
-
// Historically doing this has crashed for some reason...
|
|
641
|
-
//
|
|
642
|
-
// TODO: And we should probably free state since it's pretty large...
|
|
643
|
-
Ok(())
|
|
603
|
+
/// This is called when the thread has shut down but we can't rely on TLS destructors running,
|
|
604
|
+
/// including in multiprocess situations.
|
|
605
|
+
pub fn thread_teardown() {
|
|
606
|
+
let _ = THREAD_STATE.take();
|
|
644
607
|
}
|
|
645
608
|
|
|
646
609
|
#[unsafe(no_mangle)]
|
|
@@ -654,26 +617,20 @@ mod _functiontrace_rs {
|
|
|
654
617
|
/// This is useful when we've just forked and want to ensure we don't reuse an existing socket.
|
|
655
618
|
fn thread_reset() -> Result<()> {
|
|
656
619
|
if !STARTED.load(Ordering::Relaxed) {
|
|
657
|
-
// We haven't actually started yet, but are for some reason being asked
|
|
658
|
-
//
|
|
620
|
+
// We haven't actually started yet, but are for some reason being asked to fork. This
|
|
621
|
+
// appears to be OS dependent.
|
|
659
622
|
return Ok(());
|
|
660
623
|
}
|
|
661
624
|
|
|
662
|
-
//
|
|
663
|
-
let
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
// Free the thread's state, including the socket
|
|
669
|
-
unsafe {
|
|
670
|
-
// SAFETY: `state` came from `Box::into_raw` previously and can only be freed once.
|
|
671
|
-
let _ = Box::from_raw(state);
|
|
672
|
-
}
|
|
625
|
+
// Remove the old state without flushing the parent's socket.
|
|
626
|
+
let ThreadState { writer } = *THREAD_STATE
|
|
627
|
+
.take()
|
|
628
|
+
.ok_or_eyre(eyre!("Thread wasn't yet registered"))?;
|
|
629
|
+
let (socket, _buffer) = writer.into_parts();
|
|
630
|
+
drop(socket);
|
|
673
631
|
|
|
674
|
-
// Create a new thread for this process.
|
|
675
|
-
// with subprocess calls since we were forked and therefore already share our
|
|
676
|
-
// configuration.
|
|
632
|
+
// Create a new thread for this process. We don't need to start an entire new trace like
|
|
633
|
+
// with subprocess calls since we were forked and therefore already share our configuration.
|
|
677
634
|
thread_register().wrap_err("Failed to register new thread")?;
|
|
678
635
|
|
|
679
636
|
Ok(())
|
|
@@ -766,7 +723,7 @@ mod _functiontrace_rs {
|
|
|
766
723
|
Ok(())
|
|
767
724
|
}
|
|
768
725
|
|
|
769
|
-
/// The core logging function for FunctionTrace.
|
|
726
|
+
/// The core logging function for FunctionTrace. Configured by `PyEval_SetProfile`, and used
|
|
770
727
|
/// to record all function calls/returns.
|
|
771
728
|
extern "C" fn functiontrace(
|
|
772
729
|
_obj: *mut pyo3::ffi::PyObject,
|
|
@@ -774,142 +731,141 @@ mod _functiontrace_rs {
|
|
|
774
731
|
what: i32,
|
|
775
732
|
arg: *mut pyo3::ffi::PyObject,
|
|
776
733
|
) -> i32 {
|
|
777
|
-
let Some(mut writer) = thread_state() else {
|
|
778
|
-
// No reason to look at the event since we can't log it yet.
|
|
779
|
-
return 0;
|
|
780
|
-
};
|
|
781
|
-
|
|
782
734
|
if !STARTED.load(Ordering::Relaxed) {
|
|
783
735
|
// We aren't running (either starting up or shutting down), so shouldn't write
|
|
784
736
|
// anything.
|
|
785
737
|
return 0;
|
|
786
738
|
}
|
|
787
739
|
|
|
788
|
-
|
|
740
|
+
// We hold a mutable reference to the current ThreadState throughout this entire function.
|
|
741
|
+
// This ensures that we don't log anything from Python code that we call as part of the
|
|
742
|
+
// function (such as a `str()` implementation), since that's not part of the user's code.
|
|
743
|
+
thread_state(|mut writer| {
|
|
744
|
+
let time = trace_time();
|
|
789
745
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
746
|
+
// Return the function name when we're in a C_* context.
|
|
747
|
+
let c_func_name = || {
|
|
748
|
+
let func = arg as *mut pyo3::ffi::PyCFunctionObject;
|
|
793
749
|
|
|
794
|
-
unsafe {
|
|
795
|
-
// SAFETY: In the `C_*` cases, `arg` is a non-NULL `*PyCFunctionObject`, which
|
|
796
|
-
// always contains a valid method.
|
|
797
|
-
let method = (*func).m_ml;
|
|
798
|
-
(*method).ml_name
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
// Given a C string sourced from Python, convert it to a Cow that can be serialized.
|
|
802
|
-
let cstr_to_cow = |ptr: *const i8| {
|
|
803
|
-
if !ptr.is_null() {
|
|
804
750
|
unsafe {
|
|
805
|
-
// SAFETY: `
|
|
806
|
-
//
|
|
807
|
-
|
|
751
|
+
// SAFETY: In the `C_*` cases, `arg` is a non-NULL `*PyCFunctionObject`, which
|
|
752
|
+
// always contains a valid method.
|
|
753
|
+
let method = (*func).m_ml;
|
|
754
|
+
(*method).ml_name
|
|
808
755
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
Python::attach(|py| {
|
|
756
|
+
};
|
|
757
|
+
// Given a C string sourced from Python, convert it to a Cow that can be serialized.
|
|
758
|
+
let cstr_to_cow = |ptr: *const i8| {
|
|
759
|
+
if !ptr.is_null() {
|
|
760
|
+
unsafe {
|
|
761
|
+
// SAFETY: `ptr` is non-NULL and contains a valid C string.
|
|
762
|
+
// TODO: Can this change underneath us?
|
|
763
|
+
CStr::from_ptr(ptr)
|
|
764
|
+
}
|
|
765
|
+
.to_str()
|
|
766
|
+
.unwrap_or("<invalid string>")
|
|
767
|
+
.into()
|
|
768
|
+
} else {
|
|
769
|
+
Cow::Borrowed("<null>")
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
match what {
|
|
773
|
+
pyo3::ffi::PyTrace_CALL | pyo3::ffi::PyTrace_RETURN => {
|
|
774
|
+
// Cache information about each frame we see so that we don't need to look it up
|
|
775
|
+
// every time, which involves expensive Python operations.
|
|
776
|
+
static FRAMES: LazyLock<
|
|
777
|
+
dashmap::DashMap<usize, (Cow<'static, str>, Cow<'static, str>)>,
|
|
778
|
+
> = LazyLock::new(|| {
|
|
779
|
+
dashmap::DashMap::with_capacity(
|
|
780
|
+
// Even small programs will have a decent amount of function calls in the
|
|
781
|
+
// interpreter setup.
|
|
782
|
+
512,
|
|
783
|
+
)
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
let entry = FRAMES.entry(frame.addr()).or_insert_with(|| {
|
|
787
|
+
// We've never seen this frame before, so we'll need to load its information.
|
|
788
|
+
// This is fairly slow, since it'll involve going via Python rather than fast
|
|
789
|
+
// struct accesses.
|
|
845
790
|
let code = unsafe {
|
|
846
|
-
// SAFETY:
|
|
847
|
-
// came from `PyFrame_GetCode`.
|
|
848
|
-
Bound::from_borrowed_ptr(py, code)
|
|
849
|
-
};
|
|
791
|
+
// SAFETY: frame is guaranteed to be a valid PyFrameObject
|
|
850
792
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if let Ok(filename) = obj.cast::<pyo3::types::PyString>() {
|
|
857
|
-
Cow::from(filename.to_string_lossy().into_owned())
|
|
858
|
-
} else {
|
|
859
|
-
Cow::Borrowed("<UNKNOWN>")
|
|
860
|
-
}
|
|
861
|
-
})
|
|
862
|
-
.unwrap_or_else(|_| Cow::Borrowed("<NONE>"))
|
|
793
|
+
// We'll be storing `frame` in our hashmap, so we don't want anyone
|
|
794
|
+
// else freeing it.
|
|
795
|
+
pyo3::ffi::Py_IncRef(frame as *mut pyo3::ffi::PyObject);
|
|
796
|
+
|
|
797
|
+
pyo3::ffi::PyFrame_GetCode(frame) as *mut pyo3::ffi::PyObject
|
|
863
798
|
};
|
|
864
799
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
pyo3::intern!(py, "co_name")
|
|
872
|
-
}));
|
|
800
|
+
Python::attach(|py| {
|
|
801
|
+
let code = unsafe {
|
|
802
|
+
// SAFETY: code is guaranteed to be a valid PyCodeObject since it
|
|
803
|
+
// came from `PyFrame_GetCode`.
|
|
804
|
+
Bound::from_owned_ptr(py, code)
|
|
805
|
+
};
|
|
873
806
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
807
|
+
// Given a Python object that _should_ be a string, convert it into an
|
|
808
|
+
// a Rust string.
|
|
809
|
+
let unicode_converter = |pyobj: PyResult<Bound<'_, PyAny>>| {
|
|
810
|
+
pyobj
|
|
811
|
+
.map(|obj| {
|
|
812
|
+
if let Ok(filename) = obj.cast::<pyo3::types::PyString>() {
|
|
813
|
+
Cow::from(filename.to_string_lossy().into_owned())
|
|
814
|
+
} else {
|
|
815
|
+
Cow::Borrowed("<UNKNOWN>")
|
|
816
|
+
}
|
|
817
|
+
})
|
|
818
|
+
.unwrap_or_else(|_| Cow::Borrowed("<NONE>"))
|
|
819
|
+
};
|
|
877
820
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
821
|
+
let filename =
|
|
822
|
+
unicode_converter(code.getattr(pyo3::intern!(py, "co_filename")));
|
|
823
|
+
let func_name = unicode_converter(code.getattr(if cfg!(Py_3_11) {
|
|
824
|
+
// co_qualname is better but was only added in 3.11.
|
|
825
|
+
pyo3::intern!(py, "co_qualname")
|
|
826
|
+
} else {
|
|
827
|
+
pyo3::intern!(py, "co_name")
|
|
828
|
+
}));
|
|
882
829
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
830
|
+
(filename, func_name)
|
|
831
|
+
})
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if what == pyo3::ffi::PyTrace_CALL {
|
|
835
|
+
let linenumber = unsafe { pyo3::ffi::PyFrame_GetLineNumber(frame) }
|
|
836
|
+
.try_into()
|
|
837
|
+
.expect("Invalid line number for executing frame");
|
|
838
|
+
|
|
839
|
+
function_trace::FunctionTrace::Call {
|
|
840
|
+
filename: entry.0.clone(),
|
|
841
|
+
func_name: entry.1.clone(),
|
|
842
|
+
linenumber,
|
|
843
|
+
time,
|
|
844
|
+
}
|
|
845
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
846
|
+
.expect("Failed to emit Call message");
|
|
847
|
+
} else {
|
|
848
|
+
function_trace::FunctionTrace::Return {
|
|
849
|
+
func_name: entry.1.clone(),
|
|
850
|
+
time,
|
|
851
|
+
}
|
|
852
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
853
|
+
.expect("Failed to emit Return message")
|
|
895
854
|
}
|
|
896
|
-
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
897
|
-
.expect("Failed to emit Return message")
|
|
898
855
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
};
|
|
856
|
+
pyo3::ffi::PyTrace_C_CALL => {
|
|
857
|
+
let func = unsafe {
|
|
858
|
+
// SAFETY: In the `C_CALL` case, `arg` is guaranteed to be a non-NULL pointer
|
|
859
|
+
// to a `PyCFunctionObject`.
|
|
860
|
+
&*(arg as *mut pyo3::ffi::PyCFunctionObject)
|
|
861
|
+
};
|
|
906
862
|
|
|
907
|
-
|
|
863
|
+
let func_name = cstr_to_cow(c_func_name());
|
|
908
864
|
|
|
909
|
-
|
|
910
|
-
|
|
865
|
+
// We either belong to a module or are a method on a class.
|
|
866
|
+
if !func.m_module.is_null() {
|
|
911
867
|
// NOTE: _Typically_ `m_module` is a PyModule, but there are no requirements on
|
|
912
|
-
// what type it needs to be!
|
|
868
|
+
// what type it needs to be! In practice, we only see PyModules and PyStrings.
|
|
913
869
|
Python::attach(|py| {
|
|
914
870
|
let m_module = unsafe {
|
|
915
871
|
// SAFETY: m_module is a non-NULL pointer to a valid Python object.
|
|
@@ -973,35 +929,35 @@ mod _functiontrace_rs {
|
|
|
973
929
|
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
974
930
|
}
|
|
975
931
|
.expect("Failed to emit NativeCall message");
|
|
976
|
-
}
|
|
977
|
-
pyo3::ffi::PyTrace_C_RETURN | pyo3::ffi::PyTrace_C_EXCEPTION => {
|
|
978
|
-
if what == pyo3::ffi::PyTrace_C_EXCEPTION {
|
|
979
|
-
// C exceptions aren't exposed in a particularly interesting fashion, since they're
|
|
980
|
-
// done using the `PyErr_*` functions plus a NULL return value.
|
|
981
|
-
// However, if a `C_EXCEPTION` event is sent, we won't receive the corresponding
|
|
982
|
-
// `C_RETURN` event, so in practice we treat both of these events the same.
|
|
983
|
-
//
|
|
984
|
-
// TODO: Should we try exposing information about them? We'd probably need to
|
|
985
|
-
// change the `Exception` format in `functiontrace-server`.
|
|
986
932
|
}
|
|
933
|
+
pyo3::ffi::PyTrace_C_RETURN | pyo3::ffi::PyTrace_C_EXCEPTION => {
|
|
934
|
+
if what == pyo3::ffi::PyTrace_C_EXCEPTION {
|
|
935
|
+
// C exceptions aren't exposed in a particularly interesting fashion, since they're
|
|
936
|
+
// done using the `PyErr_*` functions plus a NULL return value.
|
|
937
|
+
// However, if a `C_EXCEPTION` event is sent, we won't receive the corresponding
|
|
938
|
+
// `C_RETURN` event, so in practice we treat both of these events the same.
|
|
939
|
+
//
|
|
940
|
+
// TODO: Should we try exposing information about them? We'd probably need to
|
|
941
|
+
// change the `Exception` format in `functiontrace-server`.
|
|
942
|
+
}
|
|
987
943
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
944
|
+
function_trace::FunctionTrace::NativeReturn {
|
|
945
|
+
func_name: cstr_to_cow(c_func_name()),
|
|
946
|
+
time,
|
|
947
|
+
}
|
|
948
|
+
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
949
|
+
.expect("Failed to emit NativeReturn message");
|
|
991
950
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
|
|
951
|
+
pyo3::ffi::PyTrace_EXCEPTION => {
|
|
952
|
+
// TODO: Actually handle exceptions and do something interesting.
|
|
953
|
+
// We unfortunately can't get these from `PyEval_SetProfile`, so we'd need to use a
|
|
954
|
+
// different method to capture them.
|
|
955
|
+
}
|
|
956
|
+
v => {
|
|
957
|
+
tracing::error!(event = v, "Unexpected PyTrace message!");
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
});
|
|
1005
961
|
0
|
|
1006
962
|
}
|
|
1007
963
|
|
|
@@ -1051,15 +1007,17 @@ mod _functiontrace_rs {
|
|
|
1051
1007
|
}
|
|
1052
1008
|
};
|
|
1053
1009
|
|
|
1054
|
-
let config = if let Ok(
|
|
1055
|
-
.wrap_err("Failed to read config file")
|
|
1056
|
-
.and_then(|x| toml::from_slice::<Config>(&x).wrap_err("Failed to parse config file"))
|
|
1057
|
-
{
|
|
1010
|
+
let config = if let Ok(contents) = std::fs::read(&config_path) {
|
|
1058
1011
|
// We successfully loaded an existing config file.
|
|
1059
|
-
|
|
1012
|
+
toml::from_slice::<Config>(&contents).wrap_err_with(|| {
|
|
1013
|
+
format!(
|
|
1014
|
+
"Failed to load existing Functiontrace config file at {}",
|
|
1015
|
+
config_path.display()
|
|
1016
|
+
)
|
|
1017
|
+
})?
|
|
1060
1018
|
} else {
|
|
1061
|
-
// There's no config file, so assume this is the first time FunctionTrace has been
|
|
1062
|
-
//
|
|
1019
|
+
// There's no config file, so assume this is the first time FunctionTrace has been used
|
|
1020
|
+
// and generate a new one.
|
|
1063
1021
|
let logo = "\
|
|
1064
1022
|
███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗████████╗██████╗ █████╗ ██████╗███████╗\n\
|
|
1065
1023
|
██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝\n\
|
|
@@ -1097,7 +1055,7 @@ mod _functiontrace_rs {
|
|
|
1097
1055
|
println!();
|
|
1098
1056
|
println!(
|
|
1099
1057
|
"{}",
|
|
1100
|
-
"Welcome to FunctionTrace, a low
|
|
1058
|
+
"Welcome to FunctionTrace, a low overhead tracing profiler for Python".bold()
|
|
1101
1059
|
);
|
|
1102
1060
|
println!();
|
|
1103
1061
|
println!(
|
|
@@ -1140,8 +1098,8 @@ mod _functiontrace_rs {
|
|
|
1140
1098
|
config
|
|
1141
1099
|
};
|
|
1142
1100
|
|
|
1143
|
-
// We need to write out the config if it's changed.
|
|
1144
|
-
//
|
|
1101
|
+
// We need to write out the config if it's changed. Track the new and old configs so we can
|
|
1102
|
+
// tell.
|
|
1145
1103
|
let mut new_config = config.clone();
|
|
1146
1104
|
|
|
1147
1105
|
////////////////////////////////////////////////////////////////////////
|
|
@@ -1292,7 +1250,7 @@ mod _functiontrace_rs {
|
|
|
1292
1250
|
if let Ok(valid_expiration) = Timestamp::strptime(DEMO_KEY_FORMAT, demo_key) {
|
|
1293
1251
|
if now
|
|
1294
1252
|
.until(valid_expiration)
|
|
1295
|
-
.
|
|
1253
|
+
.is_ok_and(|remaining| remaining.is_positive())
|
|
1296
1254
|
{
|
|
1297
1255
|
// The license is still valid
|
|
1298
1256
|
eprintln!(
|
|
@@ -1453,14 +1411,14 @@ mod allocation_wrappers {
|
|
|
1453
1411
|
|
|
1454
1412
|
/// Log the given allocation details.
|
|
1455
1413
|
fn allocation_log<F: FnOnce() -> function_trace::AllocationDetails>(msg: F) {
|
|
1456
|
-
|
|
1414
|
+
crate::_functiontrace_rs::thread_state(|mut writer| {
|
|
1457
1415
|
function_trace::FunctionTrace::Allocation {
|
|
1458
1416
|
time: crate::_functiontrace_rs::trace_time(),
|
|
1459
1417
|
details: msg(),
|
|
1460
1418
|
}
|
|
1461
1419
|
.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
1462
1420
|
.expect("Failed to emit Allocation message");
|
|
1463
|
-
}
|
|
1421
|
+
});
|
|
1464
1422
|
}
|
|
1465
1423
|
|
|
1466
1424
|
/// Given an allocator->ctx for a logging allocator, convert it to point to the original
|
|
@@ -1477,33 +1435,36 @@ mod allocation_wrappers {
|
|
|
1477
1435
|
#[unsafe(no_mangle)]
|
|
1478
1436
|
pub extern "C" fn log_malloc(ctx: *mut c_void, bytes: usize) -> *mut c_void {
|
|
1479
1437
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1480
|
-
let addr = wrapped_allocator
|
|
1438
|
+
if let Some(addr) = wrapped_allocator
|
|
1481
1439
|
.malloc
|
|
1482
1440
|
.map(|malloc| malloc(wrapped_allocator.ctx, bytes))
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1441
|
+
{
|
|
1442
|
+
allocation_log(|| function_trace::AllocationDetails::Alloc {
|
|
1443
|
+
bytes,
|
|
1444
|
+
addr: addr as usize,
|
|
1445
|
+
});
|
|
1446
|
+
addr
|
|
1447
|
+
} else {
|
|
1448
|
+
std::ptr::null_mut()
|
|
1449
|
+
}
|
|
1491
1450
|
}
|
|
1492
1451
|
|
|
1493
1452
|
#[unsafe(no_mangle)]
|
|
1494
1453
|
pub extern "C" fn log_calloc(ctx: *mut c_void, nelems: usize, elsize: usize) -> *mut c_void {
|
|
1495
1454
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1496
|
-
let addr = wrapped_allocator
|
|
1455
|
+
if let Some(addr) = wrapped_allocator
|
|
1497
1456
|
.calloc
|
|
1498
1457
|
.map(|calloc| calloc(wrapped_allocator.ctx, nelems, elsize))
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
});
|
|
1458
|
+
{
|
|
1459
|
+
allocation_log(|| function_trace::AllocationDetails::Alloc {
|
|
1460
|
+
bytes: nelems * elsize,
|
|
1461
|
+
addr: addr as usize,
|
|
1462
|
+
});
|
|
1505
1463
|
|
|
1506
|
-
|
|
1464
|
+
addr
|
|
1465
|
+
} else {
|
|
1466
|
+
std::ptr::null_mut()
|
|
1467
|
+
}
|
|
1507
1468
|
}
|
|
1508
1469
|
|
|
1509
1470
|
#[unsafe(no_mangle)]
|
|
@@ -1513,18 +1474,20 @@ mod allocation_wrappers {
|
|
|
1513
1474
|
new_size: usize,
|
|
1514
1475
|
) -> *mut c_void {
|
|
1515
1476
|
let wrapped_allocator = allocator_ctx(ctx);
|
|
1516
|
-
let addr = wrapped_allocator
|
|
1477
|
+
if let Some(addr) = wrapped_allocator
|
|
1517
1478
|
.realloc
|
|
1518
1479
|
.map(|realloc| realloc(wrapped_allocator.ctx, old_addr, new_size))
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
});
|
|
1480
|
+
{
|
|
1481
|
+
allocation_log(|| function_trace::AllocationDetails::Realloc {
|
|
1482
|
+
bytes: new_size,
|
|
1483
|
+
old_addr: old_addr as usize,
|
|
1484
|
+
new_addr: addr as usize,
|
|
1485
|
+
});
|
|
1526
1486
|
|
|
1527
|
-
|
|
1487
|
+
addr
|
|
1488
|
+
} else {
|
|
1489
|
+
std::ptr::null_mut()
|
|
1490
|
+
}
|
|
1528
1491
|
}
|
|
1529
1492
|
|
|
1530
1493
|
#[unsafe(no_mangle)]
|
|
@@ -1608,52 +1571,28 @@ mod hooks {
|
|
|
1608
1571
|
// Hooking Framework
|
|
1609
1572
|
////////////////////////////////////////////////////////////////////////////
|
|
1610
1573
|
|
|
1611
|
-
///
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
/// modify.
|
|
1621
|
-
macro_rules! hook_counter {
|
|
1622
|
-
// The accumulator has fully collected all of our hooks, so we can now emit a vec of them.
|
|
1623
|
-
( [$( ($target:literal, $wrapper:ident, $counter:expr) ),* $(,)?], $_counter:expr) => {
|
|
1624
|
-
[$( Hook {
|
|
1574
|
+
/// Generate a static slice of [`Hook`]s with info about the target functions.
|
|
1575
|
+
macro_rules! hooks {
|
|
1576
|
+
// Entry: setup a counter an accumulator array
|
|
1577
|
+
($(($target:literal, $wrapper:ident)),* $(,)?) => {
|
|
1578
|
+
hooks!(@count 0; []; $(($target, $wrapper),)*)
|
|
1579
|
+
};
|
|
1580
|
+
// Recursive case: consume one function and put info into the accumulator array.
|
|
1581
|
+
(@count $index:expr; [$($hooks:expr,)*]; ($target:literal, $wrapper:ident), $($rest:tt)*) => {
|
|
1582
|
+
hooks!(@count $index + 1; [$($hooks,)* Hook {
|
|
1625
1583
|
target: $target,
|
|
1626
|
-
wrapper: $wrapper::<{ $
|
|
1584
|
+
wrapper: $wrapper::<{ $index }>,
|
|
1627
1585
|
original: OnceLock::new(),
|
|
1628
|
-
})
|
|
1586
|
+
},]; $($rest)*)
|
|
1629
1587
|
};
|
|
1630
|
-
//
|
|
1631
|
-
(
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
( [$($acc:tt),* $(,)?], $counter:expr, $target:literal, $wrapper:ident) => {
|
|
1639
|
-
hook_counter!( [
|
|
1640
|
-
$($acc),*, ($target, $wrapper, $counter)
|
|
1641
|
-
], $counter +1)
|
|
1642
|
-
};
|
|
1643
|
-
// The main recursive case
|
|
1644
|
-
( [ $( $acc:tt ),* $(,)?], $counter:expr, $target:literal, $wrapper:ident, $($rest:tt),*) => {
|
|
1645
|
-
hook_counter!( [
|
|
1646
|
-
$($acc),*, ($target, $wrapper, $counter)
|
|
1647
|
-
], $counter +1, $($rest),*)
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
/// Generate an array of [`Hook`]s. In particular, we need to generate unique references into
|
|
1652
|
-
/// [`HOOKS`] that each wrapper is aware of, meaning we need to use a custom push-down
|
|
1653
|
-
/// accumulator and counting macro.
|
|
1654
|
-
macro_rules! hooks {
|
|
1655
|
-
($(($target:literal, $wrapper:ident)),* $(,)?) => {
|
|
1656
|
-
hook_counter!([], 0, $($target, $wrapper),*)
|
|
1588
|
+
// Base case: we have no more functions to hook, so emit the hooks.
|
|
1589
|
+
(@count $index:expr; [$($hooks:expr,)*];) => {
|
|
1590
|
+
{
|
|
1591
|
+
// We need a static array with a fixed size to get a reference to. Generate this in
|
|
1592
|
+
// the macro so the caller doesn't need to manually update the size.
|
|
1593
|
+
static ENTRIES: [Hook; $index] = [$($hooks,)*];
|
|
1594
|
+
&ENTRIES
|
|
1595
|
+
}
|
|
1657
1596
|
};
|
|
1658
1597
|
}
|
|
1659
1598
|
|
|
@@ -1662,18 +1601,19 @@ mod hooks {
|
|
|
1662
1601
|
/// NOTE: This must be created by [`hooks`], since we actually generate new variants of the
|
|
1663
1602
|
/// wrapper function for each hook, allowing the function to determine which hook triggered it
|
|
1664
1603
|
/// at runtime.
|
|
1665
|
-
static HOOKS: [Hook
|
|
1604
|
+
static HOOKS: &[Hook] = hooks!(
|
|
1605
|
+
("builtins.__import__", logging_import),
|
|
1666
1606
|
("builtins.print", logging_print),
|
|
1607
|
+
("logging.critical", logging_print),
|
|
1667
1608
|
("logging.debug", logging_print),
|
|
1668
|
-
("logging.log", logging_print),
|
|
1669
|
-
("logging.info", logging_print),
|
|
1670
|
-
("logging.warning", logging_print),
|
|
1671
1609
|
("logging.error", logging_print),
|
|
1672
|
-
("logging.critical", logging_print),
|
|
1673
|
-
("logging.fatal", logging_print),
|
|
1674
1610
|
("logging.exception", logging_print),
|
|
1675
|
-
("
|
|
1676
|
-
("
|
|
1611
|
+
("logging.fatal", logging_print),
|
|
1612
|
+
("logging.info", logging_print),
|
|
1613
|
+
("logging.log", logging_print),
|
|
1614
|
+
("logging.warning", logging_print),
|
|
1615
|
+
("multiprocessing.util._exit_function", process_exit),
|
|
1616
|
+
("os._exit", process_exit),
|
|
1677
1617
|
);
|
|
1678
1618
|
|
|
1679
1619
|
/// Information, used during both runtime and at [`install`] time, about a specific function
|
|
@@ -1690,11 +1630,13 @@ mod hooks {
|
|
|
1690
1630
|
|
|
1691
1631
|
/// A generic wrapper that parses out the arguments, calls the given handler function to handle
|
|
1692
1632
|
/// custom hooking logic, then proxies the arguments out to the original wrapped function.
|
|
1633
|
+
/// The handler emits events synchronously so they can borrow from its local Python objects.
|
|
1693
1634
|
fn proxy(
|
|
1694
1635
|
hook: &Hook,
|
|
1695
1636
|
handler: impl FnOnce(
|
|
1696
1637
|
&Bound<'_, pyo3::types::PyTuple>,
|
|
1697
|
-
|
|
1638
|
+
&mut dyn FnMut(function_trace::FunctionTrace<'_>),
|
|
1639
|
+
),
|
|
1698
1640
|
args: *mut PyObject,
|
|
1699
1641
|
kwargs: *mut PyObject,
|
|
1700
1642
|
) -> *mut PyObject {
|
|
@@ -1704,7 +1646,7 @@ mod hooks {
|
|
|
1704
1646
|
.expect("Wrapper functions can't be called until hooked");
|
|
1705
1647
|
|
|
1706
1648
|
let proxied = Python::attach(|py| -> Result<()> {
|
|
1707
|
-
|
|
1649
|
+
crate::_functiontrace_rs::thread_state(|mut writer| -> Result<()> {
|
|
1708
1650
|
let args = unsafe {
|
|
1709
1651
|
// SAFETY: `args` represents a valid argument tuple
|
|
1710
1652
|
Bound::from_borrowed_ptr(py, args)
|
|
@@ -1714,11 +1656,17 @@ mod hooks {
|
|
|
1714
1656
|
.map_err(Into::<PyErr>::into)
|
|
1715
1657
|
.wrap_err("Python args must be a tuple")?;
|
|
1716
1658
|
|
|
1717
|
-
|
|
1659
|
+
handler(args, &mut |msg| {
|
|
1718
1660
|
msg.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
|
|
1719
|
-
.
|
|
1720
|
-
|
|
1721
|
-
|
|
1661
|
+
.expect(
|
|
1662
|
+
// TODO: This isn't entirely true, but you'd have to be actively
|
|
1663
|
+
// malicious to hit this case.
|
|
1664
|
+
"Hooks emit capped size events so can't fail to serialize",
|
|
1665
|
+
);
|
|
1666
|
+
});
|
|
1667
|
+
Ok(())
|
|
1668
|
+
})
|
|
1669
|
+
.transpose()?;
|
|
1722
1670
|
|
|
1723
1671
|
Ok(())
|
|
1724
1672
|
})
|
|
@@ -1754,18 +1702,27 @@ mod hooks {
|
|
|
1754
1702
|
|
|
1755
1703
|
proxy(
|
|
1756
1704
|
hook,
|
|
1757
|
-
|args| {
|
|
1705
|
+
|args, emit| {
|
|
1758
1706
|
// Logging functions should roughly call `str()` on their arguments
|
|
1759
|
-
|
|
1760
|
-
let
|
|
1761
|
-
.
|
|
1762
|
-
.and_then(|
|
|
1763
|
-
.unwrap_or(
|
|
1707
|
+
let args = args.str().ok();
|
|
1708
|
+
let value = args
|
|
1709
|
+
.as_ref()
|
|
1710
|
+
.and_then(|args| args.to_str().ok())
|
|
1711
|
+
.unwrap_or("<invalid string>");
|
|
1764
1712
|
|
|
1765
|
-
|
|
1713
|
+
let value = if value.chars().count() > 1024 {
|
|
1714
|
+
let mut value: String = value.chars().take(1021).collect();
|
|
1715
|
+
value.push_str("...");
|
|
1716
|
+
|
|
1717
|
+
Cow::Owned(value)
|
|
1718
|
+
} else {
|
|
1719
|
+
Cow::Borrowed(value)
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
emit(function_trace::FunctionTrace::Log {
|
|
1766
1723
|
time: trace_time(),
|
|
1767
1724
|
log_type: Cow::Borrowed(hook.target),
|
|
1768
|
-
log_value:
|
|
1725
|
+
log_value: value,
|
|
1769
1726
|
})
|
|
1770
1727
|
},
|
|
1771
1728
|
args,
|
|
@@ -1783,22 +1740,22 @@ mod hooks {
|
|
|
1783
1740
|
|
|
1784
1741
|
proxy(
|
|
1785
1742
|
hook,
|
|
1786
|
-
|args| {
|
|
1743
|
+
|args, emit| {
|
|
1787
1744
|
// `import` takes rather complicated arguments, but in practice we only care about
|
|
1788
1745
|
// the first argument (name)
|
|
1789
|
-
let
|
|
1746
|
+
let arg = args.get_item(0);
|
|
1747
|
+
let module = if let Ok(arg) = &arg {
|
|
1790
1748
|
arg.cast()
|
|
1791
1749
|
.map_err(Into::into)
|
|
1792
1750
|
.and_then(|module| module.to_str())
|
|
1793
|
-
.
|
|
1794
|
-
.unwrap_or(Cow::Borrowed("<unknown module>"))
|
|
1751
|
+
.unwrap_or("<unknown module>")
|
|
1795
1752
|
} else {
|
|
1796
|
-
|
|
1753
|
+
"<no module specified>"
|
|
1797
1754
|
};
|
|
1798
1755
|
|
|
1799
|
-
|
|
1756
|
+
emit(function_trace::FunctionTrace::Import {
|
|
1800
1757
|
time: trace_time(),
|
|
1801
|
-
module_name: module,
|
|
1758
|
+
module_name: Cow::Borrowed(module),
|
|
1802
1759
|
})
|
|
1803
1760
|
},
|
|
1804
1761
|
args,
|
|
@@ -1809,21 +1766,22 @@ mod hooks {
|
|
|
1809
1766
|
/// Though this is mixed in with the rest of the hooks, this is actually load-bearing for
|
|
1810
1767
|
/// general FunctionTrace functionality.
|
|
1811
1768
|
///
|
|
1812
|
-
///
|
|
1813
|
-
///
|
|
1814
|
-
|
|
1769
|
+
/// Anything that exits with `os._exit` (including `multiprocessing` by default) will
|
|
1770
|
+
/// immediately terminate the process without running cleanup/`atexit()`. Hook this to ensure we
|
|
1771
|
+
/// can flush as needed.
|
|
1772
|
+
///
|
|
1773
|
+
/// TODO: We should also track any other outstanding threads and flush their buffers, since they
|
|
1774
|
+
/// won't get the chance to cleanly exit.
|
|
1775
|
+
extern "C" fn process_exit<const HOOK: usize>(
|
|
1815
1776
|
_self: *mut PyObject,
|
|
1816
1777
|
args: *mut PyObject,
|
|
1817
1778
|
kwargs: *mut PyObject,
|
|
1818
1779
|
) -> *mut PyObject {
|
|
1819
1780
|
let hook = &HOOKS[HOOK];
|
|
1820
1781
|
|
|
1821
|
-
|
|
1822
|
-
crate::_functiontrace_rs::thread_teardown(writer)
|
|
1823
|
-
.expect("Multiprocessing teardown should be successful");
|
|
1824
|
-
}
|
|
1782
|
+
crate::_functiontrace_rs::thread_teardown();
|
|
1825
1783
|
|
|
1826
1784
|
// Directly pass the command on, since we don't need to log/etc for this.
|
|
1827
|
-
proxy(hook, |_args|
|
|
1785
|
+
proxy(hook, |_args, _emit| {}, args, kwargs)
|
|
1828
1786
|
}
|
|
1829
1787
|
}
|
|
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
|
|
File without changes
|
|
File without changes
|