functiontrace 0.3.10__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
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.10
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>
@@ -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) {
@@ -836,10 +851,12 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
836
851
  Timestamp time = Fprofile_Time();
837
852
  char traceInitialization[1024] = { 0 };
838
853
  Writer initWriter = { 0 };
839
- intptr_t sock = -1;
840
854
  const char* version = Py_GetVersion();
841
855
  const char* platform = Py_GetPlatform();
842
856
  char* outputDirectory = NULL;
857
+ #if !defined(RUST_SOCKETS)
858
+ intptr_t sock = -1;
859
+ #endif
843
860
 
844
861
  // Parse the args to figure out what our output directory is.
845
862
  if (!PyArg_ParseTuple(args, "s", &outputDirectory)) {
@@ -848,7 +865,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
848
865
  exit(-1);
849
866
  }
850
867
 
851
- // 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.
852
884
  pid_t server = fork();
853
885
  if (server == 0) {
854
886
  // Detach ourselves as a daemon
@@ -876,6 +908,17 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
876
908
  sizeof(moduleConfiguration.socket.sun_path),
877
909
  "/tmp/functiontrace-server.sock.%d", server);
878
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)
879
922
  // Generate the TraceInitialization message to the profiler server telling
880
923
  // them about the profile we'll be creating.
881
924
  if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
@@ -902,17 +945,26 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
902
945
  usleep(1000);
903
946
  }
904
947
 
905
- // Register an env var so subprocesses know how to connect to our
906
- // profile.
907
- if (setenv(breadcrumb, (char*) &moduleConfiguration.socket.sun_path, 0)) {
908
- perror("Failed to set recursive breadcrumb");
909
- exit(-1);
910
- }
911
-
912
- // Setup a local Mpack context for this first message.
913
- mpack_writer_init(&initWriter, (char*) &traceInitialization, sizeof(traceInitialization));
914
948
  mpack_writer_set_context(&initWriter, (void*) sock);
915
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
916
968
 
917
969
  mpack_start_map(&initWriter, 5);
918
970
  {
@@ -947,10 +999,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
947
999
  // out first.
948
1000
  mpack_writer_flush_message(&initWriter);
949
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)
950
1005
  if (shutdown(sock, SHUT_WR) != 0) {
951
1006
  perror("Failed to close TraceInitialization socket");
952
1007
  exit(-1);
953
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
954
1018
  } else {
955
1019
  // Read the socket addr out of `existingProfile` so our threads know
956
1020
  // how to talk to the profile server.
@@ -1033,6 +1097,14 @@ static ThreadState* Fprofile_CreateThreadState(void) {
1033
1097
  exit(-1);
1034
1098
  }
1035
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)
1036
1108
  // Allocate a socket to report this thread's information back to the
1037
1109
  // profile server.
1038
1110
  if ((state->socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
@@ -1046,11 +1118,33 @@ static ThreadState* Fprofile_CreateThreadState(void) {
1046
1118
  exit(-1);
1047
1119
  }
1048
1120
 
1049
- // Register a messagepack writer for this thread, including telling it to
1050
- // flush on this thread's socket once the buffer fills up.
1051
- 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.
1052
1123
  mpack_writer_set_context(&state->writer, (void*) state->socket);
1053
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
1054
1148
 
1055
1149
  // Send a message about our thread's registration.
1056
1150
  Timestamp time = Fprofile_Time();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.10
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>
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.3.10"
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"},
@@ -41,5 +41,9 @@ functiontrace = "functiontrace:main"
41
41
  functiontrace = "functiontrace"
42
42
 
43
43
  [build-system]
44
- requires = ["setuptools", "toml"]
44
+ requires = ["setuptools", "toml", "setuptools-rust"]
45
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