functiontrace 0.6.0__tar.gz → 0.6.2__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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A profiling tool that traces your application's execution
5
- Author: Alistair Dobke
6
- Author-email: Matt Bryant <functiontrace@programsareproofs.com>
5
+ Author: Matt Bryant, Alistair Dobke
6
+ Maintainer-email: Matt Bryant <functiontrace@programsareproofs.com>
7
7
  License-Expression: LicenseRef-Prosperity-Public-License-3.0.0
8
8
  Project-URL: homepage, https://functiontrace.com
9
9
  Project-URL: repository, https://gitlab.com/mbryant/functiontrace
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A profiling tool that traces your application's execution
5
- Author: Alistair Dobke
6
- Author-email: Matt Bryant <functiontrace@programsareproofs.com>
5
+ Author: Matt Bryant, Alistair Dobke
6
+ Maintainer-email: Matt Bryant <functiontrace@programsareproofs.com>
7
7
  License-Expression: LicenseRef-Prosperity-Public-License-3.0.0
8
8
  Project-URL: homepage, https://functiontrace.com
9
9
  Project-URL: repository, https://gitlab.com/mbryant/functiontrace
@@ -1,11 +1,14 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "A profiling tool that traces your application's execution"
5
5
  authors = [
6
- { name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
6
+ { name = "Matt Bryant" },
7
7
  { name = "Alistair Dobke" }
8
8
  ]
9
+ maintainers = [
10
+ { name = "Matt Bryant", email = "functiontrace@programsareproofs.com" }
11
+ ]
9
12
  license = "LicenseRef-Prosperity-Public-License-3.0.0"
10
13
  license-files = ["LICENSE.md"]
11
14
  readme = "README.rst"
@@ -8,7 +8,7 @@ mod _functiontrace_rs {
8
8
  use std::borrow::Cow;
9
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,25 +29,18 @@ 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
46
  // We haven't started yet, so we shouldn't write anything out. This avoids logging
@@ -55,55 +48,12 @@ mod _functiontrace_rs {
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 = std::mem::take(&mut self.head);
92
-
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();
56
+ self.writer.flush()
107
57
  }
108
58
  }
109
59
 
@@ -427,8 +377,28 @@ mod _functiontrace_rs {
427
377
  // Close the initialization socket to trigger `functiontrace-server` to
428
378
  // read our message, since otherwise it'll wait forever for more data.
429
379
  socket
430
- .shutdown(std::net::Shutdown::Both)
431
- .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}");
432
402
  }
433
403
 
434
404
  // `multiprocessing` uses an absolute path to an executable to launch forkserver workers
@@ -616,10 +586,7 @@ mod _functiontrace_rs {
616
586
 
617
587
  // Initialize the `ThreadState` for this thread, then associate the socket with it.
618
588
  let state = Box::new(ThreadState {
619
- socket,
620
-
621
- buffer: [0u8; MSGPACK_BUFFER],
622
- head: 0,
589
+ writer: BufWriter::with_capacity(MSGPACK_BUFFER, socket),
623
590
  });
624
591
 
625
592
  // Store our state in thread-specific storage so we can find it in the future.
@@ -633,8 +600,8 @@ mod _functiontrace_rs {
633
600
  /// Tear down the given thread by dropping its [`ThreadState`], which will also flush any
634
601
  /// outstanding messages.
635
602
  ///
636
- /// This is called when the thread has shut down but we can't rely on `ThreadState::drop`
637
- /// running, including in multiprocess situations.
603
+ /// This is called when the thread has shut down but we can't rely on TLS destructors running,
604
+ /// including in multiprocess situations.
638
605
  pub fn thread_teardown() {
639
606
  let _ = THREAD_STATE.take();
640
607
  }
@@ -656,12 +623,11 @@ mod _functiontrace_rs {
656
623
  }
657
624
 
658
625
  // Remove the old state without flushing the parent's socket.
659
- let mut state = THREAD_STATE
626
+ let ThreadState { writer } = *THREAD_STATE
660
627
  .take()
661
628
  .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);
629
+ let (socket, _buffer) = writer.into_parts();
630
+ drop(socket);
665
631
 
666
632
  // Create a new thread for this process. We don't need to start an entire new trace like
667
633
  // with subprocess calls since we were forked and therefore already share our configuration.
@@ -1664,11 +1630,13 @@ mod hooks {
1664
1630
 
1665
1631
  /// A generic wrapper that parses out the arguments, calls the given handler function to handle
1666
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.
1667
1634
  fn proxy(
1668
1635
  hook: &Hook,
1669
1636
  handler: impl FnOnce(
1670
1637
  &Bound<'_, pyo3::types::PyTuple>,
1671
- ) -> Option<function_trace::FunctionTrace<'static>>,
1638
+ &mut dyn FnMut(function_trace::FunctionTrace<'_>),
1639
+ ),
1672
1640
  args: *mut PyObject,
1673
1641
  kwargs: *mut PyObject,
1674
1642
  ) -> *mut PyObject {
@@ -1688,10 +1656,14 @@ mod hooks {
1688
1656
  .map_err(Into::<PyErr>::into)
1689
1657
  .wrap_err("Python args must be a tuple")?;
1690
1658
 
1691
- if let Some(msg) = handler(args) {
1659
+ handler(args, &mut |msg| {
1692
1660
  msg.serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
1693
- .wrap_err("Failed to emit Allocation message")?;
1694
- }
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
+ });
1695
1667
  Ok(())
1696
1668
  })
1697
1669
  .transpose()?;
@@ -1730,18 +1702,27 @@ mod hooks {
1730
1702
 
1731
1703
  proxy(
1732
1704
  hook,
1733
- |args| {
1705
+ |args, emit| {
1734
1706
  // Logging functions should roughly call `str()` on their arguments
1735
- // TODO: We shouldn't need to explicitly allocate for this.
1736
- let args = args
1737
- .str()
1738
- .and_then(|x| x.to_str().map(|x| Cow::Owned(x.to_string())))
1739
- .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>");
1740
1712
 
1741
- 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 {
1742
1723
  time: trace_time(),
1743
1724
  log_type: Cow::Borrowed(hook.target),
1744
- log_value: args,
1725
+ log_value: value,
1745
1726
  })
1746
1727
  },
1747
1728
  args,
@@ -1759,22 +1740,22 @@ mod hooks {
1759
1740
 
1760
1741
  proxy(
1761
1742
  hook,
1762
- |args| {
1743
+ |args, emit| {
1763
1744
  // `import` takes rather complicated arguments, but in practice we only care about
1764
1745
  // the first argument (name)
1765
- 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 {
1766
1748
  arg.cast()
1767
1749
  .map_err(Into::into)
1768
1750
  .and_then(|module| module.to_str())
1769
- .map(|module| Cow::Owned(module.to_string()))
1770
- .unwrap_or(Cow::Borrowed("<unknown module>"))
1751
+ .unwrap_or("<unknown module>")
1771
1752
  } else {
1772
- Cow::Borrowed("<no module specified>")
1753
+ "<no module specified>"
1773
1754
  };
1774
1755
 
1775
- Some(function_trace::FunctionTrace::Import {
1756
+ emit(function_trace::FunctionTrace::Import {
1776
1757
  time: trace_time(),
1777
- module_name: module,
1758
+ module_name: Cow::Borrowed(module),
1778
1759
  })
1779
1760
  },
1780
1761
  args,
@@ -1788,6 +1769,9 @@ mod hooks {
1788
1769
  /// Anything that exits with `os._exit` (including `multiprocessing` by default) will
1789
1770
  /// immediately terminate the process without running cleanup/`atexit()`. Hook this to ensure we
1790
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.
1791
1775
  extern "C" fn process_exit<const HOOK: usize>(
1792
1776
  _self: *mut PyObject,
1793
1777
  args: *mut PyObject,
@@ -1798,6 +1782,6 @@ mod hooks {
1798
1782
  crate::_functiontrace_rs::thread_teardown();
1799
1783
 
1800
1784
  // Directly pass the command on, since we don't need to log/etc for this.
1801
- proxy(hook, |_args| None, args, kwargs)
1785
+ proxy(hook, |_args, _emit| {}, args, kwargs)
1802
1786
  }
1803
1787
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes