functiontrace 0.4.2__tar.gz → 0.4.3__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.
@@ -7,12 +7,13 @@ edition = "2021"
7
7
  color-eyre = { version = "0.6", default-features = false }
8
8
  functiontrace-server = { version = "0.8.4", default-features = false, features = ["serialize"] }
9
9
  libc = "0.2"
10
- pyo3 = { version = "0.24", features = ["eyre"] }
10
+ pyo3 = { version = "0.25", features = ["eyre"] }
11
11
  rmp-serde = "1.3"
12
12
  serde = "1"
13
13
 
14
14
  [build-dependencies]
15
15
  toml = { version = "0.8", default-features = false, features = ["parse"] }
16
+ pyo3-build-config = { version = "0.25", features = ["resolve-config"] }
16
17
 
17
18
  [lib]
18
19
  # See https://github.com/PyO3/pyo3 for details
@@ -0,0 +1,57 @@
1
+ # The Prosperity Public License 3.0.0
2
+
3
+ Contributor: Matt Bryant, Alistair Dobke
4
+
5
+ Source Code: https://gitlab.com/mbryant/functiontrace (https://functiontrace.com)
6
+
7
+ ## Purpose
8
+
9
+ This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
10
+
11
+ ## Agreement
12
+
13
+ In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
14
+
15
+ ## Notices
16
+
17
+ Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
18
+
19
+ ## Commercial Trial
20
+
21
+ Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
22
+
23
+ ## Contributions Back
24
+
25
+ Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
26
+
27
+ ## Personal Uses
28
+
29
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
30
+
31
+ ## Noncommercial Organizations
32
+
33
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
34
+
35
+ ## Defense
36
+
37
+ Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
38
+
39
+ ## Copyright
40
+
41
+ The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
42
+
43
+ ## Patent
44
+
45
+ The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
46
+
47
+ ## Reliability
48
+
49
+ The contributor can't revoke this license.
50
+
51
+ ## Excuse
52
+
53
+ You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
54
+
55
+ ## No Liability
56
+
57
+ ***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
@@ -1,23 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.4.2
3
+ Version: 0.4.3
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>
7
- License: The Prosperity Public License 3.0.0
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
10
10
  Keywords: profiler,debugger
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Topic :: Software Development :: Debuggers
13
- Classifier: License :: Free for non-commercial use
14
13
  Classifier: Operating System :: Unix
15
14
  Classifier: Environment :: Console
16
15
  Classifier: Programming Language :: Python :: 3
17
16
  Classifier: Programming Language :: Python :: 3 :: Only
18
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
- Requires-Python: >=3.8
18
+ Requires-Python: >=3.9
20
19
  Description-Content-Type: text/x-rst
20
+ License-File: LICENSE.md
21
+ Dynamic: license-file
21
22
 
22
23
  Py-Functiontrace
23
24
  ================
@@ -0,0 +1,160 @@
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
+ ////////////////////////////////////////////////////////////////////////////////
12
+ // Prototypes
13
+ ////////////////////////////////////////////////////////////////////////////////
14
+ static int Fprofile_FunctionTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg);
15
+
16
+ ////////////////////////////////////////////////////////////////////////////////
17
+ // Globals
18
+ ////////////////////////////////////////////////////////////////////////////////
19
+ // This key is used to fetch the thread local instance of the `Writer`
20
+ // structure.
21
+ static pthread_key_t Tss_Key = 0;
22
+
23
+ // The exposed apis of the Rust extension.
24
+ static struct {
25
+ pthread_key_t (*set_config)(int (*functiontrace)(PyObject*, PyFrameObject*, int, PyObject*));
26
+ }* rust;
27
+
28
+ ////////////////////////////////////////////////////////////////////////////////
29
+ // Misc
30
+ ////////////////////////////////////////////////////////////////////////////////
31
+
32
+ // Given a PyObject (likely `co_name` or similar), return a UTF-8
33
+ // representation.
34
+ static inline const char* Fprofile_UnicodeToUtf8(PyObject* obj) {
35
+ if (obj == NULL) {
36
+ return "<NULL>";
37
+ }
38
+
39
+ if (PyUnicode_Check(obj)) {
40
+ const char* utf8 = PyUnicode_AsUTF8(obj);
41
+
42
+ return utf8 != NULL ? utf8 : "<DECODE ERROR>";
43
+ } else if (obj == Py_None) {
44
+ return "<NONE>";
45
+ } else {
46
+ return "<UNKNOWN>";
47
+ }
48
+ }
49
+
50
+ ////////////////////////////////////////////////////////////////////////////////
51
+ // Callbacks
52
+ ////////////////////////////////////////////////////////////////////////////////
53
+ static int Fprofile_FunctionTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) {
54
+ struct timespec tsc = { 0 };
55
+
56
+ // This is called from the Rust code, which has already checked that
57
+ // everything is configured to send messages.
58
+ mpack_writer_t* writer = pthread_getspecific(Tss_Key);
59
+
60
+ clock_gettime(CLOCK_MONOTONIC, &tsc);
61
+
62
+ switch (what) {
63
+ case PyTrace_CALL:
64
+ mpack_start_array(writer, 5);
65
+ mpack_write_cstr(writer, "Call");
66
+
67
+ mpack_start_array(writer, 2);
68
+ mpack_write_u32(writer, tsc.tv_sec);
69
+ mpack_write_u32(writer, tsc.tv_nsec);
70
+ mpack_finish_array(writer);
71
+
72
+ {
73
+ PyCodeObject* code = PyFrame_GetCode(frame);
74
+ int lineno = PyFrame_GetLineNumber(frame);
75
+
76
+ #if PY_VERSION_HEX >= 0x030b00a0
77
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_qualname));
78
+ #else
79
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_name));
80
+ #endif
81
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_filename));
82
+ mpack_write_u32(writer, lineno);
83
+
84
+ Py_DECREF(code);
85
+ }
86
+ mpack_finish_array(writer);
87
+ break;
88
+ case PyTrace_RETURN:
89
+ mpack_start_array(writer, 3);
90
+ mpack_write_cstr(writer, "Return");
91
+
92
+ mpack_start_array(writer, 2);
93
+ mpack_write_u32(writer, tsc.tv_sec);
94
+ mpack_write_u32(writer, tsc.tv_nsec);
95
+ mpack_finish_array(writer);
96
+
97
+ {
98
+ PyCodeObject* code = PyFrame_GetCode(frame);
99
+
100
+ #if PY_VERSION_HEX >= 0x030b00a0
101
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_qualname));
102
+ #else
103
+ mpack_write_cstr(writer, Fprofile_UnicodeToUtf8(code->co_name));
104
+ #endif
105
+
106
+ Py_DECREF(code);
107
+ }
108
+ mpack_finish_array(writer);
109
+ break;
110
+ default:
111
+ perror("Impossible C case");
112
+ break;
113
+ }
114
+
115
+ return 0;
116
+ }
117
+
118
+ ////////////////////////////////////////////////////////////////////////////////
119
+ // Module Initialization
120
+ ////////////////////////////////////////////////////////////////////////////////
121
+
122
+ // The set of methods exposed by this module to Python.
123
+ static PyMethodDef methods[] = {
124
+ [0] = {NULL, NULL, 0, NULL}
125
+ };
126
+
127
+ static PyModuleDef module = {
128
+ PyModuleDef_HEAD_INIT,
129
+ "_functiontrace",
130
+ "",
131
+ -1,
132
+ methods
133
+ };
134
+
135
+ PyMODINIT_FUNC PyInit__functiontrace(void) {
136
+ // Setup our janky Rust dlopen alternative so we can use Rust code.
137
+ PyObject* rust_mod = PyImport_ImportModule("_functiontrace_rs");
138
+
139
+ // Verify that the rust extension loads if we're using it.
140
+ if (rust_mod == NULL) {
141
+ perror("Failed to load internal Rust extension");
142
+ exit(-1);
143
+ }
144
+
145
+ // This returns a pointer to the C API exposed by the Rust module.
146
+ PyObject* py_c_api = PyObject_GetAttrString(rust_mod, "c_api");
147
+ if (py_c_api == NULL) {
148
+ perror("Failed to initialize FunctionTrace Rust<->C interface");
149
+ exit(-1);
150
+ }
151
+ PyObject* c_api = PyObject_CallFunctionObjArgs(py_c_api, NULL);
152
+
153
+ // Save the Rust vtable so everyone can access it
154
+ rust = PyLong_AsVoidPtr(c_api);
155
+
156
+ // Share our globals with Rust
157
+ Tss_Key = rust->set_config(Fprofile_FunctionTrace);
158
+
159
+ return PyModule_Create(&module);
160
+ }
@@ -13,5 +13,8 @@ fn main() -> std::io::Result<()> {
13
13
  .expect("version field is present in pyproject.toml");
14
14
  println!("cargo::rustc-env=PACKAGE_VERSION={}", version);
15
15
 
16
+ // Figure out which Python version we're building for
17
+ pyo3_build_config::use_pyo3_cfgs();
18
+
16
19
  Ok(())
17
20
  }
@@ -1,23 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.4.2
3
+ Version: 0.4.3
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>
7
- License: The Prosperity Public License 3.0.0
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
10
10
  Keywords: profiler,debugger
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Topic :: Software Development :: Debuggers
13
- Classifier: License :: Free for non-commercial use
14
13
  Classifier: Operating System :: Unix
15
14
  Classifier: Environment :: Console
16
15
  Classifier: Programming Language :: Python :: 3
17
16
  Classifier: Programming Language :: Python :: 3 :: Only
18
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
- Requires-Python: >=3.8
18
+ Requires-Python: >=3.9
20
19
  Description-Content-Type: text/x-rst
20
+ License-File: LICENSE.md
21
+ Dynamic: license-file
21
22
 
22
23
  Py-Functiontrace
23
24
  ================
@@ -1,4 +1,5 @@
1
1
  Cargo.toml
2
+ LICENSE.md
2
3
  MANIFEST.in
3
4
  README.rst
4
5
  _functiontrace.c
@@ -1,20 +1,21 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "A profiling tool that traces your application's execution"
5
5
  authors = [
6
6
  { name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
7
7
  { name = "Alistair Dobke" }
8
8
  ]
9
- license = { text = "The Prosperity Public License 3.0.0" }
9
+ license = "LicenseRef-Prosperity-Public-License-3.0.0"
10
+ license-files = ["LICENSE.md"]
10
11
  readme = "README.rst"
11
- # NOTE: Actively tested on Python 3.8 - 3.13 and may plausibly break on greater
12
+ # NOTE: Actively tested on Python 3.9 - 3.13 and may plausibly break on greater
12
13
  # versions due to C extension hacks. However, setting a maximum version causes
13
14
  # new Python versions to simply install very old versions of Functiontrace that
14
15
  # didn't have the `requires-python` annotation on them and are also unlikely to
15
16
  # work. Best to only set the minimum and let bug reports notify us of issues
16
17
  # with new Python releases.
17
- requires-python = ">=3.8"
18
+ requires-python = ">=3.9"
18
19
  keywords = [
19
20
  "profiler",
20
21
  "debugger"
@@ -22,7 +23,6 @@ keywords = [
22
23
  classifiers = [
23
24
  "Intended Audience :: Developers",
24
25
  "Topic :: Software Development :: Debuggers",
25
- "License :: Free for non-commercial use",
26
26
  "Operating System :: Unix",
27
27
  "Environment :: Console",
28
28
  "Programming Language :: Python :: 3",
@@ -4,7 +4,8 @@ mod _functiontrace_rs {
4
4
  use functiontrace_server::function_trace;
5
5
  use pyo3::prelude::*; // TODO: What's in here?
6
6
  use serde::Serialize;
7
- use std::ffi::{c_void, OsString};
7
+ use std::borrow::Cow;
8
+ use std::ffi::{c_void, CStr, OsString};
8
9
  use std::io::Write;
9
10
  use std::os::unix::net::UnixStream;
10
11
  use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -46,11 +47,7 @@ mod _functiontrace_rs {
46
47
  /// A small [`Write`] implementation that allows us to write to an
47
48
  /// `mpack_writer_t`'s interior buffer while keeping its state consistent.
48
49
  fn write(&mut self, src: &[u8]) -> std::io::Result<usize> {
49
- if !STARTED
50
- .get()
51
- .expect("Set in `set_config`")
52
- .load(Ordering::Relaxed)
53
- {
50
+ if !STARTED.load(Ordering::Relaxed) {
54
51
  // We haven't started yet, so we shouldn't write anything out. This avoids logging
55
52
  // various internal setup things before we're ready.
56
53
  return Ok(src.len());
@@ -140,18 +137,11 @@ mod _functiontrace_rs {
140
137
 
141
138
  /// True iff we've started tracing and haven't been marked as terminated. When this is
142
139
  /// set, we're allowed to send messages to the profile generation server.
143
- static STARTED: OnceLock<&'static AtomicBool> = OnceLock::new();
140
+ static STARTED: AtomicBool = AtomicBool::new(false);
144
141
 
145
142
  /// The `Fprofile_FunctionTrace` function defined in the C extension. This is needed to finish
146
143
  /// moving setup to the Rust code.
147
- static TRACEFUNC: OnceLock<
148
- unsafe extern "C" fn(
149
- *mut pyo3::ffi::PyObject,
150
- *mut pyo3::ffi::PyFrameObject,
151
- i32,
152
- *mut pyo3::ffi::PyObject,
153
- ) -> i32,
154
- > = OnceLock::new();
144
+ static TRACEFUNC: OnceLock<pyo3::ffi::Py_tracefunc> = OnceLock::new();
155
145
 
156
146
  ////////////////////////////////////////////////////////////////////////////////////////////////
157
147
  // Rust <-> C Communication
@@ -176,16 +166,9 @@ mod _functiontrace_rs {
176
166
  Box::into_raw(api) as usize
177
167
  }
178
168
 
169
+ /// Called by the C module to pass the `c_functiontrace` handler to us.
179
170
  #[unsafe(no_mangle)]
180
- extern "C" fn set_config(
181
- started: *const AtomicBool,
182
- functiontrace: unsafe extern "C" fn(
183
- *mut pyo3::ffi::PyObject,
184
- *mut pyo3::ffi::PyFrameObject,
185
- i32,
186
- *mut pyo3::ffi::PyObject,
187
- ) -> i32,
188
- ) -> libc::pthread_key_t {
171
+ extern "C" fn set_config(c_functiontrace: pyo3::ffi::Py_tracefunc) -> libc::pthread_key_t {
189
172
  // Allocate a Tss_Key for storing thread state
190
173
  let mut tss_key = 0;
191
174
  if unsafe {
@@ -196,16 +179,8 @@ mod _functiontrace_rs {
196
179
  panic!("Failed to create Tss_Key");
197
180
  }
198
181
 
199
- STARTED
200
- .set(unsafe {
201
- started
202
- .as_ref()
203
- .expect("started is non-null and has a static lifetime")
204
- })
205
- .expect("set_config can only be called once");
206
-
207
182
  TRACEFUNC
208
- .set(functiontrace)
183
+ .set(c_functiontrace)
209
184
  .expect("set_config can only be called once");
210
185
 
211
186
  TSS_KEY.store(tss_key, Ordering::Relaxed);
@@ -339,7 +314,6 @@ mod _functiontrace_rs {
339
314
  module: &Bound<'_, PyModule>,
340
315
  output_directory: OsString,
341
316
  ) -> Result<()> {
342
- //fn trace_initialization(args: *mut pyo3::ffi::PyObject) -> Result<()> {
343
317
  // The name of the env var we use to notify subprocesses that there's a trace in progress.
344
318
  const BREADCRUMB: &str = "FUNCTIONTRACE_LIVE";
345
319
 
@@ -544,11 +518,8 @@ mod _functiontrace_rs {
544
518
  // Register our tracing functions with Python - both the normal one and the multithreaded
545
519
  // setup trampoline.
546
520
  unsafe {
547
- // SAFETY: tracefunc is a valid function pointer to `Fprofile_FunctionTrace`.
548
- pyo3::ffi::PyEval_SetProfile(
549
- Some(*TRACEFUNC.get().expect("tracefunc is set in set_config")),
550
- std::ptr::null_mut(),
551
- )
521
+ // SAFETY: both parameters are of the proper type.
522
+ pyo3::ffi::PyEval_SetProfile(Some(functiontrace), std::ptr::null_mut())
552
523
  };
553
524
 
554
525
  Python::with_gil(|py| -> Result<()> {
@@ -568,10 +539,7 @@ mod _functiontrace_rs {
568
539
  crate::hooks::install().wrap_err("Failed to install Python hooks")?;
569
540
 
570
541
  // We're now fully setup and allowed to send messages.
571
- STARTED
572
- .get()
573
- .expect("Set in `set_config`")
574
- .store(true, Ordering::Relaxed);
542
+ STARTED.store(true, Ordering::Relaxed);
575
543
 
576
544
  Ok(())
577
545
  }
@@ -585,10 +553,7 @@ mod _functiontrace_rs {
585
553
  _kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
586
554
  ) -> Result<()> {
587
555
  // Stop profiling and don't allow further logging, then flush any remaining messages.
588
- STARTED
589
- .get()
590
- .expect("module_configuration was set by `set_config`")
591
- .store(false, Ordering::Relaxed);
556
+ STARTED.store(false, Ordering::Relaxed);
592
557
 
593
558
  unsafe {
594
559
  // PyEval_SetProfile(NULL, NULL) is the proper way to deregister a profiler.
@@ -723,11 +688,7 @@ mod _functiontrace_rs {
723
688
  fn thread_reset() -> Result<()> {
724
689
  let tss_key = TSS_KEY.load(Ordering::Relaxed);
725
690
 
726
- if !STARTED
727
- .get()
728
- .expect("STARTED was set by `set_config`")
729
- .load(Ordering::Relaxed)
730
- {
691
+ if !STARTED.load(Ordering::Relaxed) {
731
692
  // We haven't actually started yet, but are for some reason being asked
732
693
  // to fork. This appears to be OS dependent.
733
694
  return Ok(());
@@ -782,11 +743,8 @@ mod _functiontrace_rs {
782
743
 
783
744
  // Replace our setprofile() handler with the real one.
784
745
  unsafe {
785
- // SAFETY: tracefunc is a valid function pointer to `Fprofile_FunctionTrace`.
786
- pyo3::ffi::PyEval_SetProfile(
787
- Some(*TRACEFUNC.get().expect("tracefunc is set in set_config")),
788
- std::ptr::null_mut(),
789
- )
746
+ // SAFETY: both parameters are of the proper type.
747
+ pyo3::ffi::PyEval_SetProfile(Some(functiontrace), std::ptr::null_mut())
790
748
  };
791
749
 
792
750
  // We previously called into `Fprofile_FunctionTrace` here to manually record this call,
@@ -854,6 +812,167 @@ mod _functiontrace_rs {
854
812
 
855
813
  Ok(())
856
814
  }
815
+
816
+ /// The core logging function for FunctionTrace. Configured by `PyEval_SetProfile`, and used
817
+ /// to record all function calls/returns.
818
+ extern "C" fn functiontrace(
819
+ obj: *mut pyo3::ffi::PyObject,
820
+ frame: *mut pyo3::ffi::PyFrameObject,
821
+ what: i32,
822
+ arg: *mut pyo3::ffi::PyObject,
823
+ ) -> i32 {
824
+ let Some(mut writer) = thread_state() else {
825
+ // No reason to look at the event since we can't log it yet.
826
+ return 0;
827
+ };
828
+
829
+ if !STARTED.load(Ordering::Relaxed) {
830
+ // We aren't running (either starting up or shutting down), so shouldn't write
831
+ // anything.
832
+ return 0;
833
+ }
834
+
835
+ let time = trace_time();
836
+ let c_functiontrace = *TRACEFUNC.get().expect("tracefunc is set in set_config");
837
+
838
+ // Return the function name when we're in a C_* context.
839
+ let c_func_name = || {
840
+ let func = arg as *mut pyo3::ffi::PyCFunctionObject;
841
+
842
+ unsafe {
843
+ // SAFETY: In the `C_*` cases, `arg` is a non-NULL `*PyCFunctionObject`, which
844
+ // always contains a valid method.
845
+ let method = (*func).m_ml;
846
+ (*method).ml_name
847
+ }
848
+ };
849
+ // Given a C string sourced from Python, convert it to a Cow that can be serialized.
850
+ let cstr_to_cow = |ptr: *const i8| {
851
+ if !ptr.is_null() {
852
+ unsafe {
853
+ // SAFETY: `ptr` is non-NULL and contains a valid C string.
854
+ // TODO: Can this change underneath us?
855
+ CStr::from_ptr(ptr)
856
+ }
857
+ .to_str()
858
+ .unwrap_or("<invalid string>")
859
+ .into()
860
+ } else {
861
+ Cow::Borrowed("<null>")
862
+ }
863
+ };
864
+
865
+ match what {
866
+ pyo3::ffi::PyTrace_CALL | pyo3::ffi::PyTrace_RETURN => {
867
+ // For now we delegate these to the C implementation, since we can't access the
868
+ // `PyCodeObject` fields that we require via Rust.
869
+ unsafe {
870
+ // SAFETY: tracefunc is a valid function pointer to `Fprofile_FunctionTrace`.
871
+ return c_functiontrace(obj, frame, what, arg);
872
+ }
873
+ }
874
+ pyo3::ffi::PyTrace_C_CALL => {
875
+ let func = unsafe {
876
+ // SAFETY: In the `C_CALL` case, `arg` is guaranteed to be a non-NULL pointer
877
+ // to a `PyCFunctionObject`.
878
+ &*(arg as *mut pyo3::ffi::PyCFunctionObject)
879
+ };
880
+
881
+ let func_name = cstr_to_cow(c_func_name());
882
+
883
+ // We either belong to a module or are a method on a class.
884
+ if !func.m_module.is_null() {
885
+ // NOTE: _Typically_ `m_module` is a PyModule, but there are no requirements on
886
+ // what type it needs to be! In practice, we only see PyModules and PyStrings.
887
+ Python::with_gil(|py| {
888
+ let m_module = unsafe {
889
+ // SAFETY: m_module is a non-NULL pointer to a valid Python object.
890
+ Bound::from_borrowed_ptr(py, func.m_module)
891
+ };
892
+
893
+ if let Ok(module) = m_module.downcast::<pyo3::types::PyModule>() {
894
+ // This is a PyModule, so get its name if possible.
895
+ let name = module.name();
896
+ let module_name = if let Ok(name) = &name {
897
+ name.to_cow().unwrap_or(Cow::Borrowed("<invalid string>"))
898
+ } else {
899
+ Cow::Borrowed("<unnamed module>")
900
+ };
901
+
902
+ function_trace::FunctionTrace::NativeCall {
903
+ module_name,
904
+ func_name,
905
+ time,
906
+ }
907
+ .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
908
+ } else {
909
+ let module_name =
910
+ if let Ok(module) = m_module.downcast::<pyo3::types::PyString>() {
911
+ module.to_cow().unwrap_or(Cow::Borrowed("<invalid string>"))
912
+ } else {
913
+ // TODO: tracing log this
914
+ Cow::Borrowed("<unknown module>")
915
+ };
916
+ function_trace::FunctionTrace::NativeCall {
917
+ module_name,
918
+ func_name,
919
+ time,
920
+ }
921
+ .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
922
+ }
923
+ })
924
+ } else {
925
+ // We don't belong to a module, so must be a class method.
926
+ let module_name = if !func.m_self.is_null() {
927
+ cstr_to_cow(unsafe {
928
+ // SAFETY: Since `m_self` is non-NULL, it must point to a valid
929
+ // PyObject, which is required to have a live and fully initialized
930
+ // `ob_type` member.
931
+ (*(*func.m_self).ob_type).tp_name
932
+ })
933
+ } else {
934
+ Cow::Borrowed("<unknown module>")
935
+ };
936
+
937
+ function_trace::FunctionTrace::NativeCall {
938
+ module_name,
939
+ func_name,
940
+ time,
941
+ }
942
+ .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
943
+ }
944
+ .expect("Failed to emit NativeCall message");
945
+ }
946
+ pyo3::ffi::PyTrace_C_RETURN | pyo3::ffi::PyTrace_C_EXCEPTION => {
947
+ if what == pyo3::ffi::PyTrace_C_EXCEPTION {
948
+ // C exceptions aren't exposed in a particularly interesting fashion, since they're
949
+ // done using the `PyErr_*` functions plus a NULL return value.
950
+ // However, if a `C_EXCEPTION` event is sent, we won't receive the corresponding
951
+ // `C_RETURN` event, so in practice we treat both of these events the same.
952
+ //
953
+ // TODO: Should we try exposing information about them? We'd probably need to
954
+ // change the `Exception` format in `functiontrace-server`.
955
+ }
956
+
957
+ function_trace::FunctionTrace::NativeReturn {
958
+ func_name: cstr_to_cow(c_func_name()),
959
+ time,
960
+ }
961
+ .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
962
+ .expect("Failed to emit NativeReturn message");
963
+ }
964
+ pyo3::ffi::PyTrace_EXCEPTION => {
965
+ // TODO: Actually handle exceptions and do something interesting.
966
+ // We unfortunately can't get these from `PyEval_SetProfile`, so we'd need to use a
967
+ // different method to capture them.
968
+ }
969
+ _ => {
970
+ // TODO: this shouldn't be possible - tracing log it so we can handle later.
971
+ }
972
+ };
973
+
974
+ 0
975
+ }
857
976
  }
858
977
 
859
978
  /// Allocation hooks and helpers for tracing Python memory allocator operations.
@@ -865,14 +984,12 @@ mod allocation_wrappers {
865
984
  /// Log the given allocation details.
866
985
  fn allocation_log<F: FnOnce() -> function_trace::AllocationDetails>(msg: F) {
867
986
  if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
868
- let allocation = function_trace::FunctionTrace::Allocation {
987
+ function_trace::FunctionTrace::Allocation {
869
988
  time: crate::_functiontrace_rs::trace_time(),
870
989
  details: msg(),
871
- };
872
-
873
- allocation
874
- .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
875
- .expect("Failed to emit Allocation message");
990
+ }
991
+ .serialize(&mut rmp_serde::encode::Serializer::new(&mut writer))
992
+ .expect("Failed to emit Allocation message");
876
993
  }
877
994
  }
878
995
 
@@ -1,277 +0,0 @@
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
- }
File without changes
File without changes
File without changes
File without changes