functiontrace 0.5.2__tar.gz → 0.6.0__tar.gz

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