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.
@@ -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.1
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.1
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.1"
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,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::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. This is picked somewhat arbitrarily, but should be
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
- socket: UnixStream,
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
- /// A small [`Write`] implementation that writes to a buffer and only flushes to the socket
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. This avoids logging
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
- let bytes = src.len();
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
- // We're about to send all the data, so reset our buffer. Do this now to ensure we
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. When this is
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 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()) };
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
- /// 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`.
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() -> 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")
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.truncate(95);
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::Both)
425
- .wrap_err("Failed to close initialization socket")?;
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. In
490
- // particular, we shouldn't think that we have a thread that's sent any
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). Halt our tracing instead to avoid odd behaviour and
498
- // ensure we've flushed all our data.
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` since we want to run before all
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, then flush any remaining messages.
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
- // 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
- }
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
- 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
- }
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
- // 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 {
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 flushing any outstanding messages.
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, 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(())
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
- // to fork. This appears to be OS dependent.
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
- // 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());
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. 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.
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. Configured by `PyEval_SetProfile`, and used
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
- let time = trace_time();
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
- // 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;
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: `ptr` is non-NULL and contains a valid C string.
806
- // TODO: Can this change underneath us?
807
- CStr::from_ptr(ptr)
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
- .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| {
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: code is guaranteed to be a valid PyCodeObject since it
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
- // 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>"))
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
- 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
- }));
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
- (filename, func_name)
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
- 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");
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
- 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,
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
- 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
- };
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
- let func_name = cstr_to_cow(c_func_name());
863
+ let func_name = cstr_to_cow(c_func_name());
908
864
 
909
- // We either belong to a module or are a method on a class.
910
- if !func.m_module.is_null() {
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! In practice, we only see PyModules and PyStrings.
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
- function_trace::FunctionTrace::NativeReturn {
989
- func_name: cstr_to_cow(c_func_name()),
990
- time,
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
- .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
-
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(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
- {
1010
+ let config = if let Ok(contents) = std::fs::read(&config_path) {
1058
1011
  // We successfully loaded an existing config file.
1059
- config
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
- // used and generate a new one.
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 overheard tracing profiler for Python".bold()
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. Track the new and old configs so we
1144
- // can tell.
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
- .map_or(false, |remaining| remaining.is_positive())
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
- if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
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
- .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
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
- .unwrap_or(std::ptr::null_mut());
1500
-
1501
- allocation_log(|| function_trace::AllocationDetails::Alloc {
1502
- bytes: nelems * elsize,
1503
- addr: addr as usize,
1504
- });
1458
+ {
1459
+ allocation_log(|| function_trace::AllocationDetails::Alloc {
1460
+ bytes: nelems * elsize,
1461
+ addr: addr as usize,
1462
+ });
1505
1463
 
1506
- addr
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
- .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
- });
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
- addr
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
- /// 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 {
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::<{ $counter }>,
1584
+ wrapper: $wrapper::<{ $index }>,
1627
1585
  original: OnceLock::new(),
1628
- }),*]
1586
+ },]; $($rest)*)
1629
1587
  };
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),*)
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; 11] = hooks!(
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
- ("multiprocessing.util._exit_function", multiprocessing_exit),
1676
- ("builtins.__import__", logging_import),
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
- ) -> Option<function_trace::FunctionTrace<'static>>,
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
- if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
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
- if let Some(msg) = handler(args) {
1659
+ handler(args, &mut |msg| {
1718
1660
  msg.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
1719
- .wrap_err("Failed to emit Allocation message")?;
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
- // TODO: We shouldn't need to explicitly allocate for this.
1760
- let args = args
1761
- .str()
1762
- .and_then(|x| x.to_str().map(|x| Cow::Owned(x.to_string())))
1763
- .unwrap_or(Cow::Borrowed("<invalid string>"));
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
- Some(function_trace::FunctionTrace::Log {
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: args,
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 module = if let Ok(arg) = args.get_item(0) {
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
- .map(|module| Cow::Owned(module.to_string()))
1794
- .unwrap_or(Cow::Borrowed("<unknown module>"))
1751
+ .unwrap_or("<unknown module>")
1795
1752
  } else {
1796
- Cow::Borrowed("<no module specified>")
1753
+ "<no module specified>"
1797
1754
  };
1798
1755
 
1799
- Some(function_trace::FunctionTrace::Import {
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
- /// `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>(
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
- if let Some(writer) = crate::_functiontrace_rs::thread_state() {
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| None, args, kwargs)
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