functiontrace 0.5.3__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.
@@ -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.29", features = ["eyre"] }
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.29" }
23
+ pyo3-build-config = { version = "0.30" }
24
24
 
25
25
  [lib]
26
26
  # See https://github.com/PyO3/pyo3 for details
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.5.3"
3
+ version = "0.6.0"
4
4
  description = "A profiling tool that traces your application's execution"
5
5
  authors = [
6
6
  { name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
@@ -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::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. This avoids logging
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 >= self.buffer.len() {
62
- if bytes >= self.buffer.len() {
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. The caller should retry after
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. Do this now to ensure we
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
- self.socket.write_all(&self.buffer[..head])
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. When this is
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 appears to be necessary in order to generate reasonable
116
- // assembly. Unclear why...
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
- /// Return the [`ThreadState`] for the current thread. This should either be None (during
144
- /// startup), or a valid [`ThreadState`] that has been allocated on the heap and leaked via
145
- /// `Box::into_raw`.
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() -> Option<&'static mut ThreadState> {
148
- let state = THREAD_STATE.get();
149
-
150
- if state.is_null() {
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
 
@@ -288,7 +294,7 @@ mod _functiontrace_rs {
288
294
  // We'll display this in the UI, so ensure it's under (the rather
289
295
  // arbitrary) 100 chars.
290
296
  if full_args.chars().count() > 95 {
291
- full_args.truncate(95);
297
+ full_args = full_args.chars().take(95).collect();
292
298
  full_args.push_str(" ...");
293
299
  }
294
300
 
@@ -446,6 +452,7 @@ mod _functiontrace_rs {
446
452
  };
447
453
 
448
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.
449
456
  std::fs::create_dir_all(
450
457
  wrapper
451
458
  .parent()
@@ -465,6 +472,8 @@ mod _functiontrace_rs {
465
472
  &format!("'{}'", executable.replace('\'', "'\"'\"'")),
466
473
  );
467
474
 
475
+ // TODO: this needs to be atomic, since multiple children could hit this case at
476
+ // once.
468
477
  file.write_all(script.as_bytes())
469
478
  .wrap_err("Failed to write multiprocessing wrapper")?;
470
479
  file.set_permissions(std::fs::Permissions::from_mode(0o700))
@@ -486,18 +495,17 @@ mod _functiontrace_rs {
486
495
  // Ensure we're properly handling teardown scenarios, including flushing data in both
487
496
  // single and multi-threaded scenarios, and reseting anything that's needed on forks.
488
497
 
489
- // Mark that we'll need to forget some information on forks. In
490
- // particular, we shouldn't think that we have a thread that's sent any
491
- // 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.
492
500
  if unsafe { libc::pthread_atfork(None, None, Some(c_thread_reset)) } != 0 {
493
501
  return Err(eyre!("Failed to register pthread_atfork() handler"));
494
502
  }
495
503
 
496
504
  // Things can get into a weird state during shutdown due to GC
497
- // (bugs.python.org/issue21512). Halt our tracing instead to avoid odd behaviour and
498
- // ensure we've flushed all our data.
505
+ // (bugs.python.org/issue21512). Halt our tracing instead to avoid odd behavior and ensure
506
+ // we've flushed all our data.
499
507
  //
500
- // NOTE: We specifically do this rather than `Py_AtExit` since we want to run before all
508
+ // NOTE: We specifically do this rather than `Py_AtExit` since we want to run before all
501
509
  // the Python teardown occurs.
502
510
  Python::attach(|py| -> Result<()> {
503
511
  py.import("atexit")
@@ -562,7 +570,8 @@ mod _functiontrace_rs {
562
570
  _: &Bound<'_, pyo3::types::PyTuple>,
563
571
  _kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
564
572
  ) -> Result<()> {
565
- // Stop profiling and don't allow further logging, then flush any remaining messages.
573
+ // Stop profiling and don't allow further logging.
574
+ // The outstanding messages will be flushed on exit when the ThreadState is dropped.
566
575
  STARTED.store(false, Ordering::Relaxed);
567
576
 
568
577
  unsafe {
@@ -570,23 +579,19 @@ mod _functiontrace_rs {
570
579
  pyo3::ffi::PyEval_SetProfile(None, std::ptr::null_mut());
571
580
  }
572
581
 
573
- // TODO: We could easily keep track of all the outstanding Writers and flush them all too.
574
- if let Some(writer) = thread_state() {
575
- thread_teardown(writer).wrap_err("Failed to teardown main thread")?;
576
- }
582
+ // TLS destruction isn't guaranteed to run on the main thread's shutdown.
583
+ thread_teardown();
577
584
 
578
585
  Ok(())
579
586
  }
580
587
 
581
588
  /// 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.
589
+ /// on. The new [`ThreadState`] is stored in TLS until until [`thread_teardown`] is run.
585
590
  fn thread_register() -> Result<()> {
586
- if thread_state().is_some() {
587
- // We shouldn't have a ThreadState yet, as we're the ones that create it.
588
- return Err(eyre!("Thread has already been registered"));
589
- }
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
+ );
590
595
 
591
596
  let register =
592
597
  function_trace::FunctionTrace::RegisterThread(function_trace::ThreadRegistration {
@@ -610,37 +615,28 @@ mod _functiontrace_rs {
610
615
  .wrap_err("Failed to emit RegisterThread message")?;
611
616
 
612
617
  // Initialize the `ThreadState` for this thread, then associate the socket with it.
613
- //
614
- // We'll move this state into thread local storage, and will need to recover it on
615
- // termination in order to free it.
616
- let state = Box::into_raw(Box::new(ThreadState {
618
+ let state = Box::new(ThreadState {
617
619
  socket,
618
620
 
619
621
  buffer: [0u8; MSGPACK_BUFFER],
620
622
  head: 0,
621
- }));
623
+ });
622
624
 
623
625
  // Store our state in thread-specific storage so we can find it in the future.
624
626
  // Tracing begins for the thread at this point, since we've now published `state` for other
625
627
  // functions to start using.
626
- THREAD_STATE.set(state);
628
+ THREAD_STATE.set(Some(state));
627
629
 
628
630
  Ok(())
629
631
  }
630
632
 
631
- /// Tear down the given thread by flushing any outstanding messages.
633
+ /// Tear down the given thread by dropping its [`ThreadState`], which will also flush any
634
+ /// outstanding messages.
632
635
  ///
633
- /// This is called when the thread has shut down, including in multithread/process situations.
634
- pub fn thread_teardown(writer: &mut ThreadState) -> Result<()> {
635
- writer
636
- .flush()
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(())
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();
644
640
  }
645
641
 
646
642
  #[unsafe(no_mangle)]
@@ -654,26 +650,21 @@ mod _functiontrace_rs {
654
650
  /// This is useful when we've just forked and want to ensure we don't reuse an existing socket.
655
651
  fn thread_reset() -> Result<()> {
656
652
  if !STARTED.load(Ordering::Relaxed) {
657
- // We haven't actually started yet, but are for some reason being asked
658
- // to fork. This appears to be OS dependent.
653
+ // We haven't actually started yet, but are for some reason being asked to fork. This
654
+ // appears to be OS dependent.
659
655
  return Ok(());
660
656
  }
661
657
 
662
- // Load the old state so we can free it.
663
- let state = thread_state().ok_or_eyre(eyre!("Thread wasn't yet registered"))?;
664
-
665
- // Remove the TLS reference so no one else can load state after we've freed it.
666
- THREAD_STATE.set(std::ptr::null_mut());
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);
667
665
 
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
- }
673
-
674
- // Create a new thread for this process. We don't need to start an entire new trace like
675
- // with subprocess calls since we were forked and therefore already share our
676
- // 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.
677
668
  thread_register().wrap_err("Failed to register new thread")?;
678
669
 
679
670
  Ok(())
@@ -766,7 +757,7 @@ mod _functiontrace_rs {
766
757
  Ok(())
767
758
  }
768
759
 
769
- /// The core logging function for FunctionTrace. Configured by `PyEval_SetProfile`, and used
760
+ /// The core logging function for FunctionTrace. Configured by `PyEval_SetProfile`, and used
770
761
  /// to record all function calls/returns.
771
762
  extern "C" fn functiontrace(
772
763
  _obj: *mut pyo3::ffi::PyObject,
@@ -774,142 +765,141 @@ mod _functiontrace_rs {
774
765
  what: i32,
775
766
  arg: *mut pyo3::ffi::PyObject,
776
767
  ) -> 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
768
  if !STARTED.load(Ordering::Relaxed) {
783
769
  // We aren't running (either starting up or shutting down), so shouldn't write
784
770
  // anything.
785
771
  return 0;
786
772
  }
787
773
 
788
- let time = trace_time();
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();
789
779
 
790
- // Return the function name when we're in a C_* context.
791
- let c_func_name = || {
792
- let func = arg as *mut pyo3::ffi::PyCFunctionObject;
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;
793
783
 
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
784
  unsafe {
805
- // SAFETY: `ptr` is non-NULL and contains a valid C string.
806
- // TODO: Can this change underneath us?
807
- CStr::from_ptr(ptr)
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
808
789
  }
809
- .to_str()
810
- .unwrap_or("<invalid string>")
811
- .into()
812
- } else {
813
- Cow::Borrowed("<null>")
814
- }
815
- };
816
- match what {
817
- pyo3::ffi::PyTrace_CALL | pyo3::ffi::PyTrace_RETURN => {
818
- // Cache information about each frame we see so that we don't need to look it up
819
- // every time, which involves expensive Python operations.
820
- static FRAMES: LazyLock<
821
- dashmap::DashMap<usize, (Cow<'static, str>, Cow<'static, str>)>,
822
- > = LazyLock::new(|| {
823
- dashmap::DashMap::with_capacity(
824
- // Even small programs will have a decent amount of function calls in the
825
- // interpreter setup.
826
- 512,
827
- )
828
- });
829
-
830
- let entry = FRAMES.entry(frame.addr()).or_insert_with(|| {
831
- // We've never seen this frame before, so we'll need to load its
832
- // information. This is fairly slow, since it'll involve going via Python
833
- // rather than fast struct accesses.
834
- let code = unsafe {
835
- // SAFETY: frame is guaranteed to be a valid PyFrameObject
836
-
837
- // We'll be storing `frame` in our hashmap, so we don't want anyone
838
- // else freeing it.
839
- pyo3::ffi::Py_IncRef(frame as *mut pyo3::ffi::PyObject);
840
-
841
- pyo3::ffi::PyFrame_GetCode(frame) as *mut pyo3::ffi::PyObject
842
- };
843
-
844
- 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.
845
824
  let code = unsafe {
846
- // SAFETY: code is guaranteed to be a valid PyCodeObject since it
847
- // came from `PyFrame_GetCode`.
848
- Bound::from_borrowed_ptr(py, code)
849
- };
825
+ // SAFETY: frame is guaranteed to be a valid PyFrameObject
850
826
 
851
- // Given a Python object that _should_ be a string, convert it into an
852
- // a Rust string.
853
- let unicode_converter = |pyobj: PyResult<Bound<'_, PyAny>>| {
854
- pyobj
855
- .map(|obj| {
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>"))
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
863
832
  };
864
833
 
865
- let filename =
866
- unicode_converter(code.getattr(pyo3::intern!(py, "co_filename")));
867
- let func_name = unicode_converter(code.getattr(if cfg!(Py_3_11) {
868
- // co_qualname is better but was only added in 3.11.
869
- pyo3::intern!(py, "co_qualname")
870
- } else {
871
- pyo3::intern!(py, "co_name")
872
- }));
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
+ };
873
840
 
874
- (filename, func_name)
875
- })
876
- });
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
+ };
877
854
 
878
- if what == pyo3::ffi::PyTrace_CALL {
879
- let linenumber = unsafe { pyo3::ffi::PyFrame_GetLineNumber(frame) }
880
- .try_into()
881
- .expect("Invalid line number for executing frame");
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
+ }));
882
863
 
883
- function_trace::FunctionTrace::Call {
884
- filename: entry.0.clone(),
885
- func_name: entry.1.clone(),
886
- linenumber,
887
- time,
888
- }
889
- .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
890
- .expect("Failed to emit Call message");
891
- } else {
892
- function_trace::FunctionTrace::Return {
893
- func_name: entry.1.clone(),
894
- time,
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")
895
888
  }
896
- .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
897
- .expect("Failed to emit Return message")
898
889
  }
899
- }
900
- pyo3::ffi::PyTrace_C_CALL => {
901
- let func = unsafe {
902
- // SAFETY: In the `C_CALL` case, `arg` is guaranteed to be a non-NULL pointer
903
- // to a `PyCFunctionObject`.
904
- &*(arg as *mut pyo3::ffi::PyCFunctionObject)
905
- };
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
+ };
906
896
 
907
- let func_name = cstr_to_cow(c_func_name());
897
+ let func_name = cstr_to_cow(c_func_name());
908
898
 
909
- // We either belong to a module or are a method on a class.
910
- if !func.m_module.is_null() {
899
+ // We either belong to a module or are a method on a class.
900
+ if !func.m_module.is_null() {
911
901
  // NOTE: _Typically_ `m_module` is a PyModule, but there are no requirements on
912
- // what type it needs to be! In practice, we only see PyModules and PyStrings.
902
+ // what type it needs to be! In practice, we only see PyModules and PyStrings.
913
903
  Python::attach(|py| {
914
904
  let m_module = unsafe {
915
905
  // SAFETY: m_module is a non-NULL pointer to a valid Python object.
@@ -973,35 +963,35 @@ mod _functiontrace_rs {
973
963
  .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
974
964
  }
975
965
  .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
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
+ }
987
977
 
988
- function_trace::FunctionTrace::NativeReturn {
989
- func_name: cstr_to_cow(c_func_name()),
990
- time,
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");
991
984
  }
992
- .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
993
- .expect("Failed to emit NativeReturn message");
994
- }
995
- pyo3::ffi::PyTrace_EXCEPTION => {
996
- // TODO: Actually handle exceptions and do something interesting.
997
- // We unfortunately can't get these from `PyEval_SetProfile`, so we'd need to use a
998
- // different method to capture them.
999
- }
1000
- v => {
1001
- tracing::error!(event = v, "Unexpected PyTrace message!");
1002
- }
1003
- };
1004
-
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
+ });
1005
995
  0
1006
996
  }
1007
997
 
@@ -1051,15 +1041,17 @@ mod _functiontrace_rs {
1051
1041
  }
1052
1042
  };
1053
1043
 
1054
- let config = if let Ok(config) = std::fs::read(&config_path)
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
- {
1044
+ let config = if let Ok(contents) = std::fs::read(&config_path) {
1058
1045
  // We successfully loaded an existing config file.
1059
- config
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
+ })?
1060
1052
  } else {
1061
- // There's no config file, so assume this is the first time FunctionTrace has been
1062
- // used and generate a new one.
1053
+ // There's no config file, so assume this is the first time FunctionTrace has been used
1054
+ // and generate a new one.
1063
1055
  let logo = "\
1064
1056
  ███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗████████╗██████╗ █████╗ ██████╗███████╗\n\
1065
1057
  ██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝\n\
@@ -1097,7 +1089,7 @@ mod _functiontrace_rs {
1097
1089
  println!();
1098
1090
  println!(
1099
1091
  "{}",
1100
- "Welcome to FunctionTrace, a low overheard tracing profiler for Python".bold()
1092
+ "Welcome to FunctionTrace, a low overhead tracing profiler for Python".bold()
1101
1093
  );
1102
1094
  println!();
1103
1095
  println!(
@@ -1140,8 +1132,8 @@ mod _functiontrace_rs {
1140
1132
  config
1141
1133
  };
1142
1134
 
1143
- // We need to write out the config if it's changed. Track the new and old configs so we
1144
- // can tell.
1135
+ // We need to write out the config if it's changed. Track the new and old configs so we can
1136
+ // tell.
1145
1137
  let mut new_config = config.clone();
1146
1138
 
1147
1139
  ////////////////////////////////////////////////////////////////////////
@@ -1292,7 +1284,7 @@ mod _functiontrace_rs {
1292
1284
  if let Ok(valid_expiration) = Timestamp::strptime(DEMO_KEY_FORMAT, demo_key) {
1293
1285
  if now
1294
1286
  .until(valid_expiration)
1295
- .map_or(false, |remaining| remaining.is_positive())
1287
+ .is_ok_and(|remaining| remaining.is_positive())
1296
1288
  {
1297
1289
  // The license is still valid
1298
1290
  eprintln!(
@@ -1453,14 +1445,14 @@ mod allocation_wrappers {
1453
1445
 
1454
1446
  /// Log the given allocation details.
1455
1447
  fn allocation_log<F: FnOnce() -> function_trace::AllocationDetails>(msg: F) {
1456
- if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
1448
+ crate::_functiontrace_rs::thread_state(|mut writer| {
1457
1449
  function_trace::FunctionTrace::Allocation {
1458
1450
  time: crate::_functiontrace_rs::trace_time(),
1459
1451
  details: msg(),
1460
1452
  }
1461
1453
  .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
1462
1454
  .expect("Failed to emit Allocation message");
1463
- }
1455
+ });
1464
1456
  }
1465
1457
 
1466
1458
  /// Given an allocator->ctx for a logging allocator, convert it to point to the original
@@ -1477,33 +1469,36 @@ mod allocation_wrappers {
1477
1469
  #[unsafe(no_mangle)]
1478
1470
  pub extern "C" fn log_malloc(ctx: *mut c_void, bytes: usize) -> *mut c_void {
1479
1471
  let wrapped_allocator = allocator_ctx(ctx);
1480
- let addr = wrapped_allocator
1472
+ if let Some(addr) = wrapped_allocator
1481
1473
  .malloc
1482
1474
  .map(|malloc| malloc(wrapped_allocator.ctx, bytes))
1483
- .unwrap_or(std::ptr::null_mut());
1484
-
1485
- allocation_log(|| function_trace::AllocationDetails::Alloc {
1486
- bytes,
1487
- addr: addr as usize,
1488
- });
1489
-
1490
- addr
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
+ }
1491
1484
  }
1492
1485
 
1493
1486
  #[unsafe(no_mangle)]
1494
1487
  pub extern "C" fn log_calloc(ctx: *mut c_void, nelems: usize, elsize: usize) -> *mut c_void {
1495
1488
  let wrapped_allocator = allocator_ctx(ctx);
1496
- let addr = wrapped_allocator
1489
+ if let Some(addr) = wrapped_allocator
1497
1490
  .calloc
1498
1491
  .map(|calloc| calloc(wrapped_allocator.ctx, nelems, elsize))
1499
- .unwrap_or(std::ptr::null_mut());
1500
-
1501
- allocation_log(|| function_trace::AllocationDetails::Alloc {
1502
- bytes: nelems * elsize,
1503
- addr: addr as usize,
1504
- });
1492
+ {
1493
+ allocation_log(|| function_trace::AllocationDetails::Alloc {
1494
+ bytes: nelems * elsize,
1495
+ addr: addr as usize,
1496
+ });
1505
1497
 
1506
- addr
1498
+ addr
1499
+ } else {
1500
+ std::ptr::null_mut()
1501
+ }
1507
1502
  }
1508
1503
 
1509
1504
  #[unsafe(no_mangle)]
@@ -1513,18 +1508,20 @@ mod allocation_wrappers {
1513
1508
  new_size: usize,
1514
1509
  ) -> *mut c_void {
1515
1510
  let wrapped_allocator = allocator_ctx(ctx);
1516
- let addr = wrapped_allocator
1511
+ if let Some(addr) = wrapped_allocator
1517
1512
  .realloc
1518
1513
  .map(|realloc| realloc(wrapped_allocator.ctx, old_addr, new_size))
1519
- .unwrap_or(std::ptr::null_mut());
1520
-
1521
- allocation_log(|| function_trace::AllocationDetails::Realloc {
1522
- bytes: new_size,
1523
- old_addr: old_addr as usize,
1524
- new_addr: addr as usize,
1525
- });
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
+ });
1526
1520
 
1527
- addr
1521
+ addr
1522
+ } else {
1523
+ std::ptr::null_mut()
1524
+ }
1528
1525
  }
1529
1526
 
1530
1527
  #[unsafe(no_mangle)]
@@ -1608,52 +1605,28 @@ mod hooks {
1608
1605
  // Hooking Framework
1609
1606
  ////////////////////////////////////////////////////////////////////////////
1610
1607
 
1611
- /// Implementation details behind [`hooks`].
1612
- ///
1613
- /// Keep a:
1614
- /// - counter that is incremented for each hook tuple we're passed, allowing us to know the
1615
- /// index of that hook
1616
- /// - accumulator that gathers the counted hooks, allowing us to emit a single expression at
1617
- /// the end (an array consisting of all hooks with their index associated)
1618
- ///
1619
- /// NOTE: I suspect this is grosser than it needs to be, but it works and really isn't fun to
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 {
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 {
1625
1617
  target: $target,
1626
- wrapper: $wrapper::<{ $counter }>,
1618
+ wrapper: $wrapper::<{ $index }>,
1627
1619
  original: OnceLock::new(),
1628
- }),*]
1620
+ },]; $($rest)*)
1629
1621
  };
1630
- // Handle the first iteration specifically
1631
- ( [ ], $counter:expr, $target:literal, $wrapper:ident, $($rest:tt),*) => {
1632
- hook_counter!( [
1633
- ($target, $wrapper, $counter)
1634
- ], $counter +1, $($rest),*)
1635
- };
1636
- // Handle the last iteration specifically, since we'll switch out of the normal recursive
1637
- // case and into our accumulator case.
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),*)
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
+ }
1657
1630
  };
1658
1631
  }
1659
1632
 
@@ -1662,18 +1635,19 @@ mod hooks {
1662
1635
  /// NOTE: This must be created by [`hooks`], since we actually generate new variants of the
1663
1636
  /// wrapper function for each hook, allowing the function to determine which hook triggered it
1664
1637
  /// at runtime.
1665
- static HOOKS: [Hook; 11] = hooks!(
1638
+ static HOOKS: &[Hook] = hooks!(
1639
+ ("builtins.__import__", logging_import),
1666
1640
  ("builtins.print", logging_print),
1641
+ ("logging.critical", logging_print),
1667
1642
  ("logging.debug", logging_print),
1668
- ("logging.log", logging_print),
1669
- ("logging.info", logging_print),
1670
- ("logging.warning", logging_print),
1671
1643
  ("logging.error", logging_print),
1672
- ("logging.critical", logging_print),
1673
- ("logging.fatal", logging_print),
1674
1644
  ("logging.exception", logging_print),
1675
- ("multiprocessing.util._exit_function", multiprocessing_exit),
1676
- ("builtins.__import__", logging_import),
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),
1677
1651
  );
1678
1652
 
1679
1653
  /// Information, used during both runtime and at [`install`] time, about a specific function
@@ -1704,7 +1678,7 @@ mod hooks {
1704
1678
  .expect("Wrapper functions can't be called until hooked");
1705
1679
 
1706
1680
  let proxied = Python::attach(|py| -> Result<()> {
1707
- if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
1681
+ crate::_functiontrace_rs::thread_state(|mut writer| -> Result<()> {
1708
1682
  let args = unsafe {
1709
1683
  // SAFETY: `args` represents a valid argument tuple
1710
1684
  Bound::from_borrowed_ptr(py, args)
@@ -1718,7 +1692,9 @@ mod hooks {
1718
1692
  msg.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
1719
1693
  .wrap_err("Failed to emit Allocation message")?;
1720
1694
  }
1721
- }
1695
+ Ok(())
1696
+ })
1697
+ .transpose()?;
1722
1698
 
1723
1699
  Ok(())
1724
1700
  })
@@ -1809,19 +1785,17 @@ mod hooks {
1809
1785
  /// Though this is mixed in with the rest of the hooks, this is actually load-bearing for
1810
1786
  /// general FunctionTrace functionality.
1811
1787
  ///
1812
- /// `atexit()` will only run in the main process, so we need to register a separate
1813
- /// multiprocessesing exit hook to do similar teardown on any multiprocessing workers.
1814
- extern "C" fn multiprocessing_exit<const HOOK: usize>(
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>(
1815
1792
  _self: *mut PyObject,
1816
1793
  args: *mut PyObject,
1817
1794
  kwargs: *mut PyObject,
1818
1795
  ) -> *mut PyObject {
1819
1796
  let hook = &HOOKS[HOOK];
1820
1797
 
1821
- if let Some(writer) = crate::_functiontrace_rs::thread_state() {
1822
- crate::_functiontrace_rs::thread_teardown(writer)
1823
- .expect("Multiprocessing teardown should be successful");
1824
- }
1798
+ crate::_functiontrace_rs::thread_teardown();
1825
1799
 
1826
1800
  // Directly pass the command on, since we don't need to log/etc for this.
1827
1801
  proxy(hook, |_args| None, args, kwargs)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes