functiontrace 0.3.10__tar.gz → 0.4.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.
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "_functiontrace_rs"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ color-eyre = { version = "0.6", default-features = false }
8
+ functiontrace-server = { version = "0.8.4", default-features = false, features = ["serialize"] }
9
+ libc = "0.2"
10
+ pyo3 = { version = "0.24", features = ["eyre"] }
11
+ rmp-serde = "1.3"
12
+ serde = "1"
13
+
14
+ [build-dependencies]
15
+ toml = { version = "0.8", default-features = false, features = ["parse"] }
16
+
17
+ [lib]
18
+ # See https://github.com/PyO3/pyo3 for details
19
+ name = "_functiontrace_rs" # private module to be nested into Python package
20
+ crate-type = ["cdylib"]
21
+ path = "rust/lib.rs"
@@ -0,0 +1,4 @@
1
+ graft mpack
2
+ graft rust
3
+ include Cargo.toml
4
+ include build.rs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.10
3
+ Version: 0.4.2
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.7
19
+ Requires-Python: >=3.8
20
20
  Description-Content-Type: text/x-rst
21
21
 
22
22
  Py-Functiontrace
@@ -0,0 +1,277 @@
1
+ #include <Python.h>
2
+ #include <frameobject.h>
3
+ #include <sys/types.h>
4
+ #include <pthread.h>
5
+ #include <stdio.h>
6
+ #include <stdbool.h>
7
+ #include <unistd.h>
8
+
9
+ #include "mpack/mpack.h"
10
+
11
+ //#include <assert.h>
12
+ // By default, python c extensions compile with asserts disable and it is not
13
+ // super easy to disable this behavior (AFAIK). Use this custom assert for now.
14
+ void assert_helper(int lineno, char* expr) {
15
+ printf("Assertion failed on line %d: %s\n", lineno, expr);
16
+ exit(-1);
17
+ }
18
+ #undef assert
19
+ #define assert(expr) \
20
+ if (!(expr)) \
21
+ assert_helper(__LINE__, #expr)
22
+
23
+ ////////////////////////////////////////////////////////////////////////////////
24
+ // Types
25
+ ////////////////////////////////////////////////////////////////////////////////
26
+
27
+ // The messagepack writer for the current thread.
28
+ typedef mpack_writer_t Writer;
29
+
30
+ ////////////////////////////////////////////////////////////////////////////////
31
+ // Prototypes
32
+ ////////////////////////////////////////////////////////////////////////////////
33
+ static Writer* Fprofile_Writer(void);
34
+
35
+ static int Fprofile_FunctionTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg);
36
+
37
+ ////////////////////////////////////////////////////////////////////////////////
38
+ // Globals
39
+ ////////////////////////////////////////////////////////////////////////////////
40
+ // This key is used to fetch the thread local instance of the `Writer`
41
+ // structure.
42
+ static pthread_key_t Tss_Key = 0;
43
+
44
+ // True iff we've started tracing and haven't been marked as terminated. When
45
+ // this is set, we're allowed to send messages to the profile generation
46
+ // server.
47
+ static bool started;
48
+
49
+ // The exposed apis of the Rust extension.
50
+ static struct {
51
+ pthread_key_t (*set_config)(bool* config, int (*functiontrace)(PyObject*, PyFrameObject*, int, PyObject*));
52
+ }* rust;
53
+
54
+ ////////////////////////////////////////////////////////////////////////////////
55
+ // Server Communication
56
+ ////////////////////////////////////////////////////////////////////////////////
57
+
58
+ static Writer* Fprofile_Writer(void) {
59
+ assert(Tss_Key != 0);
60
+
61
+ if (!started) {
62
+ // We aren't running (either starting up or shutting down), so
63
+ // shouldn't write anything.
64
+ return NULL;
65
+ }
66
+
67
+ // Fetch the Writerr for the current thread. It's created as late as
68
+ // the first function call for the thread, so we can plausibly get messages
69
+ // before having anywhere to store/send them to. Drop them if so - they
70
+ // weren't too important if we aren't running code yet.
71
+ Writer* state = pthread_getspecific(Tss_Key);
72
+ if (state == NULL) {
73
+ // The thread hasn't officially started yet, so drop this message.
74
+ return NULL;
75
+ }
76
+
77
+ return state;
78
+ }
79
+
80
+ ////////////////////////////////////////////////////////////////////////////////
81
+ // Misc
82
+ ////////////////////////////////////////////////////////////////////////////////
83
+
84
+ // Given a PyObject (likely `co_name` or similar), return a UTF-8
85
+ // representation.
86
+ static inline const char* Fprofile_UnicodeToUtf8(PyObject* obj) {
87
+ if (obj == NULL) {
88
+ return "<NULL>";
89
+ }
90
+
91
+ if (PyUnicode_Check(obj)) {
92
+ const char* utf8 = PyUnicode_AsUTF8(obj);
93
+
94
+ return utf8 != NULL ? utf8 : "<DECODE ERROR>";
95
+ } else if (obj == Py_None) {
96
+ return "<NONE>";
97
+ } else {
98
+ return "<UNKNOWN>";
99
+ }
100
+ }
101
+
102
+ ////////////////////////////////////////////////////////////////////////////////
103
+ // Callbacks
104
+ ////////////////////////////////////////////////////////////////////////////////
105
+ static int Fprofile_FunctionTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) {
106
+ PyCFunctionObject* fn = (PyCFunctionObject*) arg;
107
+ struct timespec tsc = { 0 };
108
+
109
+ Writer* writer = Fprofile_Writer();
110
+ if (writer == NULL) {
111
+ return 0;
112
+ }
113
+
114
+ clock_gettime(CLOCK_MONOTONIC, &tsc);
115
+
116
+ switch (what) {
117
+ case PyTrace_CALL:
118
+ mpack_start_array(writer, 5);
119
+ mpack_write_cstr(writer, "Call");
120
+
121
+ mpack_start_array(writer, 2);
122
+ mpack_write_u32(writer, tsc.tv_sec);
123
+ mpack_write_u32(writer, tsc.tv_nsec);
124
+ mpack_finish_array(writer);
125
+
126
+ {
127
+ // TODO: Check if frame is none or similar
128
+ #if PY_VERSION_HEX >= 0x030a00a0
129
+ // Python >= 3.10 should use the standard accessors
130
+ PyCodeObject* code = PyFrame_GetCode(frame);
131
+ int lineno = PyFrame_GetLineNumber(frame);
132
+ #else
133
+ PyCodeObject* code = frame->f_code;
134
+ int lineno = frame->f_lineno;
135
+ #endif
136
+
137
+ #if PY_VERSION_HEX >= 0x030b00a0
138
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_qualname));
139
+ #else
140
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_name));
141
+ #endif
142
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_filename));
143
+ mpack_write_u32(writer, lineno);
144
+
145
+ #if PY_VERSION_HEX >= 0x030a00a0
146
+ Py_DECREF(code);
147
+ #endif
148
+ }
149
+ mpack_finish_array(writer);
150
+ break;
151
+ case PyTrace_RETURN:
152
+ mpack_start_array(writer, 3);
153
+ mpack_write_cstr(writer, "Return");
154
+
155
+ mpack_start_array(writer, 2);
156
+ mpack_write_u32(writer, tsc.tv_sec);
157
+ mpack_write_u32(writer, tsc.tv_nsec);
158
+ mpack_finish_array(writer);
159
+
160
+ {
161
+ #if PY_VERSION_HEX >= 0x030a00a0
162
+ PyCodeObject* code = PyFrame_GetCode(frame);
163
+ #else
164
+ PyCodeObject* code = frame->f_code;
165
+ #endif
166
+ #if PY_VERSION_HEX >= 0x030b00a0
167
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_qualname));
168
+ #else
169
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_name));
170
+ #endif
171
+
172
+ #if PY_VERSION_HEX >= 0x030a00a0
173
+ Py_DECREF(code);
174
+ #endif
175
+ }
176
+ mpack_finish_array(writer);
177
+ break;
178
+ case PyTrace_C_CALL: {
179
+ mpack_start_array(writer, 4);
180
+ mpack_write_cstr(writer, "NativeCall");
181
+
182
+ mpack_start_array(writer, 2);
183
+ mpack_write_u32(writer, tsc.tv_sec);
184
+ mpack_write_u32(writer, tsc.tv_nsec);
185
+ mpack_finish_array(writer);
186
+
187
+ {
188
+ // Attempt to determine what module/class this function belongs
189
+ // to.
190
+ PyObject* self = fn->m_self;
191
+ PyObject* module = fn->m_module;
192
+ const char* name = fn->m_ml->ml_name;
193
+ const char* module_name = NULL;
194
+
195
+ // Check if we belong to a module, and if not we must be a
196
+ // method. We do this order to avoid finding that the object
197
+ // we belong to is of type module.
198
+ if (module != NULL) {
199
+ if (PyModule_Check(module)) {
200
+ module_name = PyModule_GetName(module);
201
+ } else if (PyUnicode_Check(module)) {
202
+ module_name = PyUnicode_AsUTF8(module);
203
+ }
204
+ } else if (self != NULL) {
205
+ // This is a method call on a class.
206
+ module_name = self->ob_type->tp_name;
207
+ }
208
+
209
+ mpack_write_cstr(writer, name != NULL ? name : "NULL");
210
+ mpack_write_cstr(writer, module_name != NULL ? module_name : "NULL");
211
+ }
212
+ mpack_finish_array(writer);
213
+ break;
214
+ }
215
+ case PyTrace_C_RETURN:
216
+ mpack_start_array(writer, 3);
217
+ mpack_write_cstr(writer, "NativeReturn");
218
+
219
+ mpack_start_array(writer, 2);
220
+ mpack_write_u32(writer, tsc.tv_sec);
221
+ mpack_write_u32(writer, tsc.tv_nsec);
222
+ mpack_finish_array(writer);
223
+
224
+ {
225
+ const char* name = fn->m_ml->ml_name;
226
+ mpack_write_cstr(writer, name != NULL ? name : "NULL");
227
+ }
228
+ mpack_finish_array(writer);
229
+ break;
230
+ default:
231
+ // TODO: We should handle exceptions here (or somewhere similar).
232
+ break;
233
+ }
234
+
235
+ return 0;
236
+ }
237
+
238
+ ////////////////////////////////////////////////////////////////////////////////
239
+ // Module Initialization
240
+ ////////////////////////////////////////////////////////////////////////////////
241
+
242
+ // The set of methods exposed by this module to Python.
243
+ static PyMethodDef methods[] = {
244
+ [0] = {NULL, NULL, 0, NULL}
245
+ };
246
+
247
+ static PyModuleDef module = {
248
+ PyModuleDef_HEAD_INIT,
249
+ "_functiontrace",
250
+ "",
251
+ -1,
252
+ methods
253
+ };
254
+
255
+ PyMODINIT_FUNC PyInit__functiontrace(void) {
256
+ // Setup our janky Rust dlopen alternative so we can use Rust code.
257
+ PyObject* rust_mod = PyImport_ImportModule("_functiontrace_rs");
258
+
259
+ // Verify that the rust extension loads if we're using it.
260
+ if (rust_mod == NULL) {
261
+ perror("Failed to load internal Rust extension");
262
+ exit(-1);
263
+ }
264
+
265
+ // This returns a pointer to the C API exposed by the Rust module.
266
+ PyObject* py_c_api = PyObject_GetAttrString(rust_mod, "c_api");
267
+ assert(py_c_api != NULL);
268
+ PyObject* c_api = PyObject_CallFunctionObjArgs(py_c_api, NULL);
269
+
270
+ // Save the Rust vtable so everyone can access it
271
+ rust = PyLong_AsVoidPtr(c_api);
272
+
273
+ // Share our globals with Rust
274
+ Tss_Key = rust->set_config(&started, Fprofile_FunctionTrace);
275
+
276
+ return PyModule_Create(&module);
277
+ }
@@ -0,0 +1,17 @@
1
+ fn main() -> std::io::Result<()> {
2
+ println!("cargo::rerun-if-changed=pyproject.toml");
3
+
4
+ // Read pyproject.toml to expose our version to the Rust extension
5
+ let pyproject = std::fs::read_to_string("pyproject.toml")?
6
+ .parse::<toml::Table>()
7
+ .expect("pyproject.toml is valid toml");
8
+ let version = pyproject
9
+ .get("project")
10
+ .and_then(|p| p.as_table())
11
+ .and_then(|p| p.get("version"))
12
+ .and_then(|v| v.as_str())
13
+ .expect("version field is present in pyproject.toml");
14
+ println!("cargo::rustc-env=PACKAGE_VERSION={}", version);
15
+
16
+ Ok(())
17
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.3.10
3
+ Version: 0.4.2
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.7
19
+ Requires-Python: >=3.8
20
20
  Description-Content-Type: text/x-rst
21
21
 
22
22
  Py-Functiontrace
@@ -1,6 +1,8 @@
1
+ Cargo.toml
1
2
  MANIFEST.in
2
3
  README.rst
3
4
  _functiontrace.c
5
+ build.rs
4
6
  functiontrace.py
5
7
  pyproject.toml
6
8
  setup.py
@@ -11,4 +13,5 @@ functiontrace.egg-info/entry_points.txt
11
13
  functiontrace.egg-info/top_level.txt
12
14
  mpack/LICENSE
13
15
  mpack/mpack.c
14
- mpack/mpack.h
16
+ mpack/mpack.h
17
+ rust/lib.rs
@@ -5,7 +5,8 @@ import sys
5
5
  import tempfile
6
6
  from importlib import machinery, util
7
7
 
8
- import _functiontrace
8
+ import _functiontrace # noqa: F401 - this import is load bearing since we need to run code on import
9
+ import _functiontrace_rs as rust
9
10
 
10
11
  PYTHON_TEMPLATE = """#!/bin/sh
11
12
 
@@ -70,9 +71,7 @@ def main():
70
71
  action="store_true",
71
72
  help="Disable compression of traces, which can be unnecessarily slow if filesize is not a concern.",
72
73
  )
73
- parser.add_argument(
74
- "-v", "--version", action="version", version=_functiontrace.__version__
75
- )
74
+ parser.add_argument("-v", "--version", action="version", version=rust.__version__)
76
75
  parser.add_argument("script", nargs=argparse.REMAINDER)
77
76
  args = parser.parse_args()
78
77
 
@@ -114,8 +113,8 @@ def main():
114
113
  if args.no_compression:
115
114
  os.environ["FUNCTIONTRACE_COMPRESSION"] = "false"
116
115
  if args.trace_memory:
117
- _functiontrace.config_tracememory()
118
- _functiontrace.begin_tracing(args.output_dir)
116
+ rust.enable_tracememory()
117
+ rust.begin_tracing(args.output_dir)
119
118
 
120
119
  # Run their code now that we're tracing. This must be done in the context
121
120
  # of the __main__ module we've created.
@@ -125,7 +124,7 @@ def main():
125
124
  def trace():
126
125
  # Make sure we're set up to work properly, then begin tracing.
127
126
  setup_dependencies()
128
- _functiontrace.begin_tracing(os.getcwd())
127
+ rust.begin_tracing(os.getcwd())
129
128
 
130
129
 
131
130
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.3.10"
3
+ version = "0.4.2"
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,13 +8,13 @@ authors = [
8
8
  ]
9
9
  license = { text = "The Prosperity Public License 3.0.0" }
10
10
  readme = "README.rst"
11
- # NOTE: Actively tested on Python 3.7 - 3.12 and may plausibly break on greater
11
+ # NOTE: Actively tested on Python 3.8 - 3.13 and may plausibly break on greater
12
12
  # versions due to C extension hacks. However, setting a maximum version causes
13
13
  # new Python versions to simply install very old versions of Functiontrace that
14
14
  # didn't have the `requires-python` annotation on them and are also unlikely to
15
15
  # work. Best to only set the minimum and let bug reports notify us of issues
16
16
  # with new Python releases.
17
- requires-python = ">=3.7"
17
+ requires-python = ">=3.8"
18
18
  keywords = [
19
19
  "profiler",
20
20
  "debugger"
@@ -41,5 +41,9 @@ functiontrace = "functiontrace:main"
41
41
  functiontrace = "functiontrace"
42
42
 
43
43
  [build-system]
44
- requires = ["setuptools", "toml"]
44
+ requires = ["setuptools", "setuptools-rust"]
45
45
  build-backend = "setuptools.build_meta"
46
+
47
+ [[tool.setuptools-rust.ext-modules]]
48
+ target = "_functiontrace_rs" # Must match `lib.name` in Cargo.toml
49
+ binding = "PyO3"