functiontrace 0.3.7__tar.gz → 0.4.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.
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "_functiontrace-rs"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7
+
8
+ [dependencies]
9
+ pyo3 = "0.24"
10
+
11
+ [profile.release-lto]
12
+ inherits = "release"
13
+ lto = true
14
+
15
+ [lib]
16
+ # See https://github.com/PyO3/pyo3 for details
17
+ name = "_lib" # private module to be nested into Python package
18
+ crate-type = ["cdylib"]
19
+ path = "rust/lib.rs"
@@ -0,0 +1,3 @@
1
+ graft mpack
2
+ graft rust
3
+ include Cargo.toml
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.7
3
+ Version: 0.4.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>
@@ -16,7 +16,7 @@ Classifier: Environment :: Console
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
- Requires-Python: <3.12,>=3.6
19
+ Requires-Python: >=3.7
20
20
  Description-Content-Type: text/x-rst
21
21
 
22
22
  Py-Functiontrace
@@ -10,6 +10,9 @@
10
10
 
11
11
  #include "mpack/mpack.h"
12
12
 
13
+ // We use the Rust implementation for all data transport.
14
+ #define RUST_SOCKETS
15
+
13
16
  //#include <assert.h>
14
17
  // By default, python c extensions compile with asserts disable and it is not
15
18
  // super easy to disable this behavior (AFAIK). Use this custom assert for now.
@@ -56,7 +59,9 @@ typedef struct {
56
59
  // Prototypes
57
60
  ////////////////////////////////////////////////////////////////////////////////
58
61
  static Writer* Fprofile_Writer(void);
62
+ #if !defined(RUST_SOCKETS)
59
63
  static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes);
64
+ #endif
60
65
 
61
66
  static Timestamp Fprofile_Time(void);
62
67
 
@@ -117,6 +122,11 @@ static struct {
117
122
  // The Unix socket address we're using to communicate with the profile
118
123
  // server.
119
124
  struct sockaddr_un socket;
125
+
126
+ #if defined(RUST_SOCKETS)
127
+ // The Rust extension.
128
+ PyObject* rust;
129
+ #endif
120
130
  } moduleConfiguration;
121
131
 
122
132
  // The set of methods exposed by this module to Python.
@@ -146,7 +156,7 @@ static PyMethodDef methods[] = {
146
156
  .ml_flags = METH_VARARGS,
147
157
  .ml_doc = "INTERNAL USE ONLY"
148
158
  },
149
- [METHOD_TERMINATE ] = {
159
+ [METHOD_TERMINATE] = {
150
160
  .ml_name = "terminate",
151
161
  .ml_meth = (PyCFunction) Fprofile_Terminate,
152
162
  .ml_flags = METH_NOARGS,
@@ -181,6 +191,7 @@ static Writer* Fprofile_Writer(void) {
181
191
  return &state->writer;
182
192
  }
183
193
 
194
+ #if !defined(RUST_SOCKETS)
184
195
  static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes) {
185
196
  // The context passed to Mpack is the socket to send on.
186
197
  intptr_t socket = (intptr_t) writer->context;
@@ -190,6 +201,7 @@ static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes) {
190
201
  exit(-1);
191
202
  }
192
203
  }
204
+ #endif
193
205
 
194
206
  ////////////////////////////////////////////////////////////////////////////////
195
207
  // Misc
@@ -732,6 +744,9 @@ static void Fprofile_ThreadTeardown(void* key) {
732
744
  }
733
745
 
734
746
  mpack_writer_flush_message(&state->writer);
747
+
748
+ // TODO: We should tear down the socket to avoid leaking O(# threads)
749
+ // resources.
735
750
  }
736
751
 
737
752
  static PyObject* Fprofile_MpexitHook(HookedFunction* hook, PyObject* args, PyObject* kwargs) {
@@ -804,6 +819,14 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
804
819
  }
805
820
  }
806
821
 
822
+ if (moduleConfiguration.pid != 0) {
823
+ // This can be triggered by running `functiontrace.begin_tracing()`
824
+ // from inside a process that's already being traced.
825
+ // We'll fail in this situation, but might as well print a mildly
826
+ // helpful error message here.
827
+ perror("Functiontrace doubly initialized! Aborting.");
828
+ exit(-1);
829
+ }
807
830
  moduleConfiguration.pid = getpid();
808
831
 
809
832
  // Mark that we'll need to forget some information on forks. In
@@ -828,10 +851,12 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
828
851
  Timestamp time = Fprofile_Time();
829
852
  char traceInitialization[1024] = { 0 };
830
853
  Writer initWriter = { 0 };
831
- intptr_t sock = -1;
832
854
  const char* version = Py_GetVersion();
833
855
  const char* platform = Py_GetPlatform();
834
856
  char* outputDirectory = NULL;
857
+ #if !defined(RUST_SOCKETS)
858
+ intptr_t sock = -1;
859
+ #endif
835
860
 
836
861
  // Parse the args to figure out what our output directory is.
837
862
  if (!PyArg_ParseTuple(args, "s", &outputDirectory)) {
@@ -840,7 +865,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
840
865
  exit(-1);
841
866
  }
842
867
 
843
- // Spawn the server and wait for it to connect.
868
+ #if defined(RUST_SOCKETS)
869
+ // NOTE: We only import this module once. Importing while tracing is
870
+ // live will be recorded, which exposes internals users don't care
871
+ // about and breaks the invariant that we send a `RegisterThread`
872
+ // message first when threads are traced.
873
+ moduleConfiguration.rust = PyImport_ImportModule("_functiontrace_rs._lib");
874
+
875
+ // Verify that the rust extension loads if we're using it.
876
+ if (moduleConfiguration.rust == NULL) {
877
+ perror("Failed to load internal Rust extension");
878
+ exit(-1);
879
+ }
880
+ #endif
881
+
882
+ // Spawn the server. We'll wait a bit for it to start accepting
883
+ // sockets.
844
884
  pid_t server = fork();
845
885
  if (server == 0) {
846
886
  // Detach ourselves as a daemon
@@ -868,6 +908,17 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
868
908
  sizeof(moduleConfiguration.socket.sun_path),
869
909
  "/tmp/functiontrace-server.sock.%d", server);
870
910
 
911
+ // Register an env var so subprocesses know how to connect to our
912
+ // profile.
913
+ if (setenv(breadcrumb, (char*) &moduleConfiguration.socket.sun_path, 0)) {
914
+ perror("Failed to set recursive breadcrumb");
915
+ exit(-1);
916
+ }
917
+
918
+ // Setup a local Mpack context for this first message.
919
+ mpack_writer_init(&initWriter, (char*) &traceInitialization, sizeof(traceInitialization));
920
+
921
+ #if !defined(RUST_SOCKETS)
871
922
  // Generate the TraceInitialization message to the profiler server telling
872
923
  // them about the profile we'll be creating.
873
924
  if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
@@ -894,17 +945,26 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
894
945
  usleep(1000);
895
946
  }
896
947
 
897
- // Register an env var so subprocesses know how to connect to our
898
- // profile.
899
- if (setenv(breadcrumb, (char*) &moduleConfiguration.socket.sun_path, 0)) {
900
- perror("Failed to set recursive breadcrumb");
901
- exit(-1);
902
- }
903
-
904
- // Setup a local Mpack context for this first message.
905
- mpack_writer_init(&initWriter, (char*) &traceInitialization, sizeof(traceInitialization));
906
948
  mpack_writer_set_context(&initWriter, (void*) sock);
907
949
  mpack_writer_set_flush(&initWriter, &Mpack_Flush);
950
+ #else
951
+ {
952
+ PyObject* msg_init = PyObject_GetAttrString(moduleConfiguration.rust, "message_initialization");
953
+ assert(msg_init != NULL);
954
+
955
+ PyObject* arg = PyLong_FromVoidPtr(&moduleConfiguration.socket.sun_path);
956
+ PyObject* context = PyObject_CallFunctionObjArgs(msg_init, arg, NULL);
957
+ assert(context != NULL);
958
+
959
+ PyObject* msg_flush = PyObject_GetAttrString(moduleConfiguration.rust, "message_flush");
960
+ assert(msg_flush != NULL);
961
+ PyObject* mpack_flush = PyObject_CallFunctionObjArgs(msg_flush, NULL);
962
+ assert(mpack_flush != NULL);
963
+
964
+ mpack_writer_set_context(&initWriter, PyLong_AsVoidPtr(context));
965
+ mpack_writer_set_flush(&initWriter, PyLong_AsVoidPtr(mpack_flush));
966
+ }
967
+ #endif
908
968
 
909
969
  mpack_start_map(&initWriter, 5);
910
970
  {
@@ -939,10 +999,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
939
999
  // out first.
940
1000
  mpack_writer_flush_message(&initWriter);
941
1001
 
1002
+ // Close the initialization socket to trigger `functiontrace-server` to
1003
+ // read our message, since otherwise it'll wait forever for more data.
1004
+ #if !defined(RUST_SOCKETS)
942
1005
  if (shutdown(sock, SHUT_WR) != 0) {
943
1006
  perror("Failed to close TraceInitialization socket");
944
1007
  exit(-1);
945
1008
  }
1009
+ #else
1010
+ {
1011
+ PyObject* msg_shutdown = PyObject_GetAttrString(moduleConfiguration.rust, "message_shutdown");
1012
+ assert(msg_shutdown != NULL);
1013
+
1014
+ PyObject* socket = PyLong_FromVoidPtr(mpack_writer_context(&initWriter));
1015
+ assert(PyObject_CallFunctionObjArgs(msg_shutdown, socket, NULL) != NULL);
1016
+ }
1017
+ #endif
946
1018
  } else {
947
1019
  // Read the socket addr out of `existingProfile` so our threads know
948
1020
  // how to talk to the profile server.
@@ -1025,6 +1097,14 @@ static ThreadState* Fprofile_CreateThreadState(void) {
1025
1097
  exit(-1);
1026
1098
  }
1027
1099
 
1100
+ // Setup msgpack and a socket for this thread to report tracing info on.
1101
+ //
1102
+ // We need to do this before any allocations or Python function calls
1103
+ // occur, since consumers can't tell whether mpack is fully initialized or
1104
+ // not.
1105
+ mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
1106
+
1107
+ #if !defined(RUST_SOCKETS)
1028
1108
  // Allocate a socket to report this thread's information back to the
1029
1109
  // profile server.
1030
1110
  if ((state->socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
@@ -1038,11 +1118,33 @@ static ThreadState* Fprofile_CreateThreadState(void) {
1038
1118
  exit(-1);
1039
1119
  }
1040
1120
 
1041
- // Register a messagepack writer for this thread, including telling it to
1042
- // flush on this thread's socket once the buffer fills up.
1043
- mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
1121
+ // Configure Mpack to flush events to this thread's socket once the buffer
1122
+ // fills up.
1044
1123
  mpack_writer_set_context(&state->writer, (void*) state->socket);
1045
1124
  mpack_writer_set_flush(&state->writer, &Mpack_Flush);
1125
+ #else
1126
+ {
1127
+ PyObject* msg_init = PyObject_GetAttrString(moduleConfiguration.rust, "message_initialization");
1128
+ assert(msg_init != NULL);
1129
+
1130
+ // TODO: Convert this to a shared Rust configuration object so we don't
1131
+ // need to pass it back and forth.
1132
+ PyObject* arg = PyLong_FromVoidPtr(&moduleConfiguration.socket.sun_path);
1133
+ PyObject* context = PyObject_CallFunctionObjArgs(msg_init, arg, NULL);
1134
+ assert(context != NULL);
1135
+
1136
+ PyObject* msg_flush = PyObject_GetAttrString(moduleConfiguration.rust, "message_flush");
1137
+ assert(msg_flush != NULL);
1138
+ PyObject* mpack_flush = PyObject_CallFunctionObjArgs(msg_flush, NULL);
1139
+ assert(mpack_flush != NULL);
1140
+
1141
+ // Re-initialize msgpack, since we've done some allocations and Python
1142
+ // calls that we don't want to actually record.
1143
+ mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
1144
+ mpack_writer_set_context(&state->writer, PyLong_AsVoidPtr(context));
1145
+ mpack_writer_set_flush(&state->writer, PyLong_AsVoidPtr(mpack_flush));
1146
+ }
1147
+ #endif
1046
1148
 
1047
1149
  // Send a message about our thread's registration.
1048
1150
  Timestamp time = Fprofile_Time();
@@ -1099,4 +1201,3 @@ static void Fprofile_ResetThreadState(void) {
1099
1201
  moduleConfiguration.pid = getpid();
1100
1202
  Fprofile_CreateThreadState();
1101
1203
  }
1102
-
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.7
3
+ Version: 0.4.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>
@@ -16,7 +16,7 @@ Classifier: Environment :: Console
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
- Requires-Python: <3.12,>=3.6
19
+ Requires-Python: >=3.7
20
20
  Description-Content-Type: text/x-rst
21
21
 
22
22
  Py-Functiontrace
@@ -1,3 +1,4 @@
1
+ Cargo.toml
1
2
  MANIFEST.in
2
3
  README.rst
3
4
  _functiontrace.c
@@ -11,4 +12,5 @@ functiontrace.egg-info/entry_points.txt
11
12
  functiontrace.egg-info/top_level.txt
12
13
  mpack/LICENSE
13
14
  mpack/mpack.c
14
- mpack/mpack.h
15
+ mpack/mpack.h
16
+ rust/lib.rs
@@ -41,7 +41,7 @@ def setup_dependencies():
41
41
  # Generate wrappers for the various Python versions we support to ensure
42
42
  # they're included in our PATH.
43
43
  wrap_pythons = ["python", "python3"] + [
44
- "python3.{}".format(minor) for minor in range(6, 12)
44
+ "python3.{}".format(minor) for minor in range(6, 13)
45
45
  ]
46
46
  for python in wrap_pythons:
47
47
  with open(os.path.join(tempdir, python), "w") as f:
@@ -55,9 +55,7 @@ def main():
55
55
  "--trace-memory",
56
56
  "--trace_memory",
57
57
  action="store_true",
58
- help="""Trace
59
- memory allocations/frees when enabled. This may add
60
- tracing overhead, so is disabled by default.""",
58
+ help="Trace memory allocations/frees when enabled. This may add tracing overhead, so is disabled by default.",
61
59
  )
62
60
  parser.add_argument(
63
61
  "--output-dir",
@@ -66,6 +64,12 @@ def main():
66
64
  default=os.getcwd(),
67
65
  help="The directory to output trace files to",
68
66
  )
67
+ parser.add_argument(
68
+ "--no-compression",
69
+ "--no_compression",
70
+ action="store_true",
71
+ help="Disable compression of traces, which can be unnecessarily slow if filesize is not a concern.",
72
+ )
69
73
  parser.add_argument(
70
74
  "-v", "--version", action="version", version=_functiontrace.__version__
71
75
  )
@@ -107,6 +111,8 @@ def main():
107
111
  setup_dependencies()
108
112
 
109
113
  # Setup our tracing environment, including configuring tracing features.
114
+ if args.no_compression:
115
+ os.environ["FUNCTIONTRACE_COMPRESSION"] = "false"
110
116
  if args.trace_memory:
111
117
  _functiontrace.config_tracememory()
112
118
  _functiontrace.begin_tracing(args.output_dir)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.3.7"
3
+ version = "0.4.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"},
@@ -8,7 +8,13 @@ authors = [
8
8
  ]
9
9
  license = { text = "The Prosperity Public License 3.0.0" }
10
10
  readme = "README.rst"
11
- requires-python = ">=3.6,<3.12"
11
+ # NOTE: Actively tested on Python 3.7 - 3.12 and may plausibly break on greater
12
+ # versions due to C extension hacks. However, setting a maximum version causes
13
+ # new Python versions to simply install very old versions of Functiontrace that
14
+ # didn't have the `requires-python` annotation on them and are also unlikely to
15
+ # work. Best to only set the minimum and let bug reports notify us of issues
16
+ # with new Python releases.
17
+ requires-python = ">=3.7"
12
18
  keywords = [
13
19
  "profiler",
14
20
  "debugger"
@@ -35,5 +41,9 @@ functiontrace = "functiontrace:main"
35
41
  functiontrace = "functiontrace"
36
42
 
37
43
  [build-system]
38
- requires = ["setuptools", "toml"]
44
+ requires = ["setuptools", "toml", "setuptools-rust"]
39
45
  build-backend = "setuptools.build_meta"
46
+
47
+ [[tool.setuptools-rust.ext-modules]]
48
+ target = "_functiontrace_rs._lib" # Must match `lib.name` in Cargo.toml
49
+ binding = "PyO3"
@@ -0,0 +1,100 @@
1
+ #[pyo3::pymodule]
2
+ mod _lib {
3
+ use pyo3::prelude::*;
4
+ use std::ffi::{CStr, OsStr};
5
+ use std::os::raw::c_char;
6
+ use std::os::unix::ffi::OsStrExt;
7
+ use std::os::unix::net::UnixStream;
8
+
9
+ ////////////////////////////////////////////////////////////////////////////////////////////////
10
+ // Socket Communication
11
+ ////////////////////////////////////////////////////////////////////////////////////////////////
12
+
13
+ /// Initializes and returns a pointer to the [`UnixStream`] we'll communicate to
14
+ /// `functiontrace-server` on.
15
+ ///
16
+ /// The returned value will be stored as [`MpackWriter.context`] and used by [`Mpack_Flush`].
17
+ #[pyfunction]
18
+ fn message_initialization(socket: usize) -> usize {
19
+ let c_socket = unsafe {
20
+ // SAFETY: socket is a valid `char*` converted via `PyLong_FromVoidPtr`.
21
+ CStr::from_ptr(socket as *const c_char)
22
+ };
23
+ let sockaddr = OsStr::from_bytes(c_socket.to_bytes());
24
+
25
+ // The functiontrace-server might not be ready to receive connections yet, so we retry for
26
+ // a bit.
27
+ let start = std::time::Instant::now();
28
+ let socket = loop {
29
+ match UnixStream::connect(sockaddr) {
30
+ Ok(s) => {
31
+ break Box::new(s);
32
+ }
33
+ _ => {
34
+ if start.elapsed() > std::time::Duration::from_millis(1000) {
35
+ panic!("Timed out trying to connect to functiontrace-server");
36
+ }
37
+
38
+ std::thread::sleep(std::time::Duration::from_millis(10));
39
+ }
40
+ }
41
+ };
42
+
43
+ // This must be freed by `message_shutdown`
44
+ Box::into_raw(socket) as usize
45
+ }
46
+
47
+ /// Shutdown a socket that was opened by [`message_initialize`], triggering
48
+ /// `functiontrace-server` to read the data sent over it.
49
+ ///
50
+ /// If we forget this, we'll leak sockets and a bit of memory.
51
+ #[pyfunction]
52
+ fn message_shutdown(socket: usize) {
53
+ let socket = unsafe {
54
+ // SAFETY: socket is a `UnixStream` emitted from `message_initialization` encoded via
55
+ // `PyLong_FromVoidPtr`.
56
+ Box::from_raw(socket as *mut UnixStream)
57
+ };
58
+
59
+ if let Err(e) = socket.shutdown(std::net::Shutdown::Both) {
60
+ panic!("Failed to close socket: {e}");
61
+ }
62
+ }
63
+
64
+ /// The initial subset of the `mpack_writer_t` struct, which `mpack.h` will use to pass us our
65
+ /// per-thread context in.
66
+ #[repr(C)]
67
+ struct MpackWriter {
68
+ flush: *const (),
69
+ error_fn: *const (),
70
+ teardown: *const (),
71
+ context: *mut UnixStream,
72
+ }
73
+
74
+ /// Flush events to this socket once the given buffer is full according to `mpack`.
75
+ #[unsafe(no_mangle)]
76
+ extern "C" fn Mpack_Flush(writer: *const MpackWriter, buffer: *const u8, bytes: usize) {
77
+ use std::io::Write;
78
+
79
+ let data = unsafe {
80
+ // SAFETY: `bytes` represents the initialized number of bytes in `buffer` when called
81
+ // by `mpack_writer_flush_message`.
82
+ std::slice::from_raw_parts(buffer, bytes)
83
+ };
84
+
85
+ let mut socket = unsafe {
86
+ // SAFETY: `writer` is valid and `writer->context` contains a `UnixStream` emitted from
87
+ // `message_initialization`.
88
+ &*(*writer).context
89
+ };
90
+ if let Err(e) = socket.write_all(data) {
91
+ panic!("Socket send failed: {e}");
92
+ }
93
+ }
94
+
95
+ /// C accessor for [`Mpack_Flush`]
96
+ #[pyfunction]
97
+ fn message_flush() -> PyResult<usize> {
98
+ Ok(Mpack_Flush as usize)
99
+ }
100
+ }
@@ -1 +0,0 @@
1
- graft mpack
File without changes
File without changes
File without changes