pStatClient_ext.cxx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. /**
  2. * PANDA 3D SOFTWARE
  3. * Copyright (c) Carnegie Mellon University. All rights reserved.
  4. *
  5. * All use of this software is subject to the terms of the revised BSD
  6. * license. You should have received a copy of this license along
  7. * with this source code in a file named "LICENSE."
  8. *
  9. * @file pStatClient_ext.cxx
  10. * @author rdb
  11. * @date 2022-11-23
  12. */
  13. #include "pStatClient_ext.h"
  14. #if defined(HAVE_PYTHON) && defined(DO_PSTATS)
  15. #include "pStatCollector.h"
  16. #include "config_pstatclient.h"
  17. #include "reMutexHolder.h"
  18. #ifndef CPPPARSER
  19. #include "frameobject.h"
  20. #endif
  21. static bool _python_profiler_enabled = false;
  22. // Used to cache stuff onto PyCodeObjects.
  23. static Py_ssize_t _extra_index = -1;
  24. // Stores a mapping between C method definitions and collector indices.
  25. static pmap<PyMethodDef *, int> _c_method_collectors;
  26. // Parent collector for all Python profiling collectors.
  27. static PStatCollector code_collector("App:Python");
  28. // Parent collector for all Python ref tracer collectors.
  29. static PStatCollector refs_collector("Python objects");
  30. /**
  31. * Walks up the type hierarchy to find the class where the method originates.
  32. */
  33. static bool
  34. find_method(PyTypeObject *&cls, PyObject *name, PyCodeObject *code) {
  35. PyObject *meth = _PyType_Lookup(cls, name);
  36. if (meth == nullptr || !PyFunction_Check(meth) ||
  37. PyFunction_GET_CODE(meth) != (PyObject *)code) {
  38. return false;
  39. }
  40. if (cls->tp_bases != nullptr) {
  41. Py_ssize_t size = PyTuple_GET_SIZE(cls->tp_bases);
  42. for (Py_ssize_t i = 0; i < size; ++i) {
  43. PyTypeObject *base = (PyTypeObject *)PyTuple_GET_ITEM(cls->tp_bases, i);
  44. if (find_method(base, name, code)) {
  45. cls = base;
  46. return true;
  47. }
  48. }
  49. }
  50. // Didn't find it in any of the bases, it must be defined here.
  51. return true;
  52. }
  53. /**
  54. * Returns the collector for a Python frame.
  55. */
  56. static int
  57. #ifdef __GNUC__
  58. __attribute__ ((noinline))
  59. #elif defined(_MSC_VER)
  60. __declspec(noinline)
  61. #endif
  62. make_python_frame_collector(PyFrameObject *frame, PyCodeObject *code) {
  63. #if PY_VERSION_HEX >= 0x030B0000 // 3.11
  64. // Fetch the module name out of the frame's global scope.
  65. const char *mod_name = "<unknown>";
  66. PyObject *py_mod_name = nullptr;
  67. PyObject *globals = PyFrame_GetGlobals(frame);
  68. #if PY_VERSION_HEX >= 0x030D00A1 // 3.13
  69. if (PyDict_GetItemStringRef(globals, "__name__", &py_mod_name) > 0) {
  70. mod_name = PyUnicode_AsUTF8(py_mod_name);
  71. }
  72. #else
  73. py_mod_name = PyDict_GetItemString(globals, "__name__");
  74. if (py_mod_name != nullptr) {
  75. mod_name = PyUnicode_AsUTF8(py_mod_name);
  76. }
  77. #endif
  78. Py_DECREF(globals);
  79. const char *meth_name = PyUnicode_AsUTF8(code->co_qualname);
  80. char buffer[1024];
  81. size_t len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, meth_name);
  82. for (size_t i = 0; i < len - 1; ++i) {
  83. if (buffer[i] == '.') {
  84. buffer[i] = ':';
  85. }
  86. }
  87. #if PY_VERSION_HEX >= 0x030D00A1 // 3.13
  88. Py_XDECREF(py_mod_name);
  89. #endif
  90. #else
  91. // Try to figure out the type name. There's no obvious way to do this.
  92. // It's possible that the first argument passed to this function is the
  93. // self instance or the current type (for a classmethod), but we have to
  94. // double-check that to make sure.
  95. PyTypeObject *cls = nullptr;
  96. if (code->co_argcount >= 1) {
  97. PyFrame_FastToLocals(frame);
  98. PyObject *first_arg = PyDict_GetItem(frame->f_locals, PyTuple_GET_ITEM(code->co_varnames, 0));
  99. cls = PyType_Check(first_arg) ? (PyTypeObject *)first_arg : Py_TYPE(first_arg);
  100. if ((cls->tp_flags & Py_TPFLAGS_HEAPTYPE) != 0) {
  101. // Mangling scheme for methods starting (but not ending) with "__"
  102. PyObject *meth_name = code->co_name;
  103. Py_ssize_t len = PyUnicode_GET_LENGTH(meth_name);
  104. if (len >= 2 && PyUnicode_READ_CHAR(meth_name, 0) == '_' && PyUnicode_READ_CHAR(meth_name, 1) == '_' &&
  105. (len < 4 || PyUnicode_READ_CHAR(meth_name, len - 1) != '_' || PyUnicode_READ_CHAR(meth_name, len - 2) != '_')) {
  106. const char *cls_name = cls->tp_name;
  107. while (cls_name[0] == '_') {
  108. ++cls_name;
  109. }
  110. meth_name = PyUnicode_FromFormat("_%s%S", cls_name, meth_name);
  111. } else {
  112. meth_name = Py_NewRef(meth_name);
  113. }
  114. if (!find_method(cls, meth_name, code)) {
  115. // Not a matching method object, it's something else. Forget it.
  116. cls = nullptr;
  117. }
  118. Py_DECREF(meth_name);
  119. } else {
  120. cls = nullptr;
  121. }
  122. }
  123. // Fetch the module name out of the frame's global scope.
  124. PyObject *py_mod_name = PyDict_GetItemString(frame->f_globals, "__name__");
  125. if (py_mod_name == nullptr && cls != nullptr) {
  126. py_mod_name = PyDict_GetItemString(cls->tp_dict, "__module__");
  127. }
  128. const char *mod_name = py_mod_name ? PyUnicode_AsUTF8(py_mod_name) : "<unknown>";
  129. char buffer[1024];
  130. size_t len = snprintf(buffer, sizeof(buffer), "%s:", mod_name);
  131. for (size_t i = 0; i < len - 1; ++i) {
  132. if (buffer[i] == '.') {
  133. buffer[i] = ':';
  134. }
  135. }
  136. const char *meth_name = PyUnicode_AsUTF8(code->co_name);
  137. if (cls != nullptr) {
  138. len += snprintf(buffer + len, sizeof(buffer) - len, "%s:%s", cls->tp_name, meth_name);
  139. } else {
  140. len += snprintf(buffer + len, sizeof(buffer) - len, "%s", meth_name);
  141. }
  142. #endif
  143. // Add parentheses, unless it's something special like <listcomp>
  144. if (len < sizeof(buffer) - 2 && buffer[len - 1] != '>') {
  145. buffer[len++] = '(';
  146. buffer[len++] = ')';
  147. buffer[len] = '\0';
  148. }
  149. PStatCollector collector(code_collector, buffer);
  150. intptr_t collector_index = collector.get_index();
  151. if (_extra_index != -1) {
  152. _PyCode_SetExtra((PyObject *)code, _extra_index, (void *)collector_index);
  153. }
  154. return collector_index;
  155. }
  156. /**
  157. * Creates a collector for a C function.
  158. */
  159. static int
  160. #ifdef __GNUC__
  161. __attribute__ ((noinline))
  162. #elif defined(_MSC_VER)
  163. __declspec(noinline)
  164. #endif
  165. make_c_function_collector(PyCFunctionObject *meth) {
  166. char buffer[1024];
  167. size_t len;
  168. if (meth->m_self != nullptr && !PyModule_Check(meth->m_self)) {
  169. PyTypeObject *cls = PyType_Check(meth->m_self) ? (PyTypeObject *)meth->m_self : Py_TYPE(meth->m_self);
  170. const char *dot = strrchr(cls->tp_name, '.');
  171. if (dot != nullptr) {
  172. // The module name is included in the type name.
  173. snprintf(buffer, sizeof(buffer), "%s:%s()", cls->tp_name, meth->m_ml->ml_name);
  174. len = (dot - cls->tp_name) + 1;
  175. } else {
  176. // If there's no module name, we need to get it from __module__.
  177. PyObject *py_mod_name = nullptr;
  178. const char *mod_name = nullptr;
  179. if (cls->tp_dict != nullptr &&
  180. PyDict_GetItemStringRef(cls->tp_dict, "__module__", &py_mod_name) > 0) {
  181. if (PyUnicode_Check(py_mod_name)) {
  182. mod_name = PyUnicode_AsUTF8(py_mod_name);
  183. } else {
  184. // Might be a descriptor.
  185. Py_DECREF(py_mod_name);
  186. py_mod_name = PyObject_GetAttrString(meth->m_self, "__module__");
  187. if (py_mod_name != nullptr) {
  188. if (PyUnicode_Check(py_mod_name)) {
  189. mod_name = PyUnicode_AsUTF8(py_mod_name);
  190. }
  191. }
  192. else PyErr_Clear();
  193. }
  194. }
  195. else PyErr_Clear();
  196. if (mod_name == nullptr) {
  197. // Is it a built-in, like int or dict?
  198. PyObject *builtins = PyEval_GetBuiltins();
  199. if (PyDict_GetItemString(builtins, cls->tp_name) == (PyObject *)cls) {
  200. mod_name = "builtins";
  201. } else {
  202. mod_name = "<unknown>";
  203. }
  204. }
  205. len = snprintf(buffer, sizeof(buffer), "%s:%s:%s()", mod_name, cls->tp_name, meth->m_ml->ml_name) - 2;
  206. Py_XDECREF(py_mod_name);
  207. }
  208. }
  209. else if (meth->m_self != nullptr) {
  210. const char *mod_name = PyModule_GetName(meth->m_self);
  211. len = snprintf(buffer, sizeof(buffer), "%s:%s()", mod_name, meth->m_ml->ml_name) - 2;
  212. }
  213. else {
  214. snprintf(buffer, sizeof(buffer), "%s()", meth->m_ml->ml_name);
  215. len = 0;
  216. }
  217. for (size_t i = 0; i < len; ++i) {
  218. if (buffer[i] == '.') {
  219. buffer[i] = ':';
  220. }
  221. }
  222. PStatCollector collector(code_collector, buffer);
  223. int collector_index = collector.get_index();
  224. _c_method_collectors[meth->m_ml] = collector.get_index();
  225. return collector_index;
  226. }
  227. /**
  228. * Attempts to establish a connection to the indicated PStatServer. Returns
  229. * true if successful, false on failure.
  230. */
  231. bool Extension<PStatClient>::
  232. client_connect(std::string hostname, int port) {
  233. extern struct Dtool_PyTypedObject Dtool_PStatThread;
  234. if (_this->client_connect(std::move(hostname), port)) {
  235. // Pass a PStatThread as argument.
  236. if (!_python_profiler_enabled && pstats_python_profiler) {
  237. PStatThread *thread = new PStatThread(_this->get_current_thread());
  238. PyObject *arg = DTool_CreatePyInstance((void *)thread, Dtool_PStatThread, true, false);
  239. if (_extra_index == -1) {
  240. _extra_index = _PyEval_RequestCodeExtraIndex(nullptr);
  241. }
  242. PyEval_SetProfile(&trace_callback, arg);
  243. _python_profiler_enabled = true;
  244. }
  245. // We require 3.14a6, since that version fixes an important bug with the
  246. // ref tracer; prior versions did not properly send destroy events.
  247. #if PY_VERSION_HEX >= 0x030E0000
  248. if (Py_Version >= 0x030E00A6) {
  249. if (pstats_python_ref_tracer) {
  250. PyRefTracer_SetTracer(&ref_trace_callback, _this);
  251. }
  252. }
  253. else
  254. #endif
  255. if (pstats_python_ref_tracer) {
  256. pstats_cat.warning()
  257. << "The pstats-python-ref-tracer feature requires at least "
  258. "Python 3.14a6.\n";
  259. }
  260. return true;
  261. }
  262. else if (_python_profiler_enabled) {
  263. PyEval_SetProfile(nullptr, nullptr);
  264. _python_profiler_enabled = false;
  265. }
  266. return false;
  267. }
  268. /**
  269. * Closes the connection previously established.
  270. */
  271. void Extension<PStatClient>::
  272. client_disconnect() {
  273. _this->client_disconnect();
  274. if (_python_profiler_enabled) {
  275. PyEval_SetProfile(nullptr, nullptr);
  276. _python_profiler_enabled = false;
  277. }
  278. #if PY_VERSION_HEX >= 0x030E0000 // 3.14
  279. void *data;
  280. if (PyRefTracer_GetTracer(&data) == &ref_trace_callback && data == _this) {
  281. PyRefTracer_SetTracer(nullptr, nullptr);
  282. }
  283. #endif
  284. }
  285. /**
  286. * Callback passed to PyEval_SetProfile.
  287. */
  288. int Extension<PStatClient>::
  289. trace_callback(PyObject *py_thread, PyFrameObject *frame, int what, PyObject *arg) {
  290. intptr_t collector_index;
  291. if (what == PyTrace_CALL || what == PyTrace_RETURN || what == PyTrace_EXCEPTION) {
  292. // Normal Python frame entry/exit.
  293. #if PY_VERSION_HEX >= 0x030B0000 // 3.11
  294. PyCodeObject *code = PyFrame_GetCode(frame);
  295. #else
  296. PyCodeObject *code = frame->f_code;
  297. #endif
  298. // The index for this collector is cached on the code object.
  299. if (_PyCode_GetExtra((PyObject *)code, _extra_index, (void **)&collector_index) != 0 || collector_index == 0) {
  300. collector_index = make_python_frame_collector(frame, code);
  301. }
  302. #if PY_VERSION_HEX >= 0x030B0000 // 3.11
  303. Py_DECREF(code);
  304. #endif
  305. } else if (what == PyTrace_C_CALL || what == PyTrace_C_RETURN || what == PyTrace_C_EXCEPTION) {
  306. // Call to a C function or method, which has no frame of its own.
  307. if (PyCFunction_CheckExact(arg)) {
  308. PyCFunctionObject *meth = (PyCFunctionObject *)arg;
  309. auto it = _c_method_collectors.find(meth->m_ml);
  310. if (it != _c_method_collectors.end()) {
  311. collector_index = it->second;
  312. } else {
  313. collector_index = make_c_function_collector(meth);
  314. }
  315. } else {
  316. return 0;
  317. }
  318. } else {
  319. return 0;
  320. }
  321. if (collector_index <= 0) {
  322. return 0;
  323. }
  324. PStatThread &pthread = *(PStatThread *)DtoolInstance_VOID_PTR(py_thread);
  325. PStatClient *client = pthread.get_client();
  326. if (!client->client_is_connected()) {
  327. // Client was disconnected, disable Python profiling.
  328. PyEval_SetProfile(nullptr, nullptr);
  329. _python_profiler_enabled = false;
  330. return 0;
  331. }
  332. int thread_index = pthread.get_index();
  333. #ifdef _DEBUG
  334. nassertr(collector_index >= 0 && collector_index < client->get_num_collectors(), -1);
  335. nassertr(thread_index >= 0 && thread_index < client->get_num_threads(), -1);
  336. #endif
  337. PStatClient::Collector *collector = client->get_collector_ptr(collector_index);
  338. PStatClient::InternalThread *thread = client->get_thread_ptr(thread_index);
  339. if (collector->is_active() && thread->_is_active) {
  340. double as_of = client->get_real_time();
  341. LightMutexHolder holder(thread->_thread_lock);
  342. if (thread->_thread_active) {
  343. if (what == PyTrace_CALL || what == PyTrace_C_CALL) {
  344. thread->_frame_data.add_start(collector_index, as_of);
  345. } else {
  346. thread->_frame_data.add_stop(collector_index, as_of);
  347. }
  348. }
  349. }
  350. return 0;
  351. }
  352. /**
  353. * Callback passed to PyRefTracer_SetTracer.
  354. */
  355. #if PY_VERSION_HEX >= 0x030E0000 // 3.14
  356. int Extension<PStatClient>::
  357. ref_trace_callback(PyObject *obj, PyRefTracerEvent event, void *data) {
  358. PStatClient *client = (PStatClient *)data;
  359. if (!client->client_is_connected()) {
  360. return 0;
  361. }
  362. PyTypeObject *cls = Py_TYPE(obj);
  363. #ifdef Py_GIL_DISABLED
  364. // With GIL disabled, the GIL is no longer protecting the cache, so we
  365. // have to do that ourselves.
  366. client->_lock.acquire();
  367. #endif
  368. int collector_index;
  369. auto it = client->_python_type_collectors.find(cls);
  370. if (it != client->_python_type_collectors.end()) {
  371. collector_index = it->second;
  372. #ifdef Py_GIL_DISABLED
  373. client->_lock.release();
  374. #endif
  375. }
  376. else {
  377. #ifdef Py_GIL_DISABLED
  378. client->_lock.release();
  379. #endif
  380. char buffer[1024];
  381. size_t len;
  382. if (cls == &PyDict_Type || cls == &PyUnicode_Type) {
  383. // Prevents recursion due to PyDict_GetItemStringRef
  384. len = snprintf(buffer, sizeof(buffer), "builtins:%s", cls->tp_name);
  385. }
  386. else {
  387. const char *dot = strrchr(cls->tp_name, '.');
  388. if (dot != nullptr) {
  389. // The module name is included in the type name.
  390. len = snprintf(buffer, sizeof(buffer), "%s", cls->tp_name);
  391. } else {
  392. // If there's no module name, we need to get it from __module__.
  393. PyObject *py_mod_name = nullptr;
  394. const char *mod_name = nullptr;
  395. if (cls->tp_dict != nullptr &&
  396. PyDict_GetItemStringRef(cls->tp_dict, "__module__", &py_mod_name) > 0) {
  397. if (PyUnicode_Check(py_mod_name)) {
  398. mod_name = PyUnicode_AsUTF8(py_mod_name);
  399. } else {
  400. // Might be a descriptor.
  401. Py_DECREF(py_mod_name);
  402. py_mod_name = PyObject_GetAttrString(obj, "__module__");
  403. if (py_mod_name != nullptr) {
  404. if (PyUnicode_Check(py_mod_name)) {
  405. mod_name = PyUnicode_AsUTF8(py_mod_name);
  406. }
  407. }
  408. else PyErr_Clear();
  409. }
  410. }
  411. else PyErr_Clear();
  412. if (mod_name == nullptr) {
  413. // Is it a built-in, like int or dict?
  414. PyObject *builtins = PyEval_GetBuiltins();
  415. if (PyDict_GetItemString(builtins, cls->tp_name) == (PyObject *)cls) {
  416. mod_name = "builtins";
  417. } else {
  418. mod_name = "<unknown>";
  419. }
  420. }
  421. len = snprintf(buffer, sizeof(buffer), "%s:%s", mod_name, cls->tp_name);
  422. Py_XDECREF(py_mod_name);
  423. }
  424. }
  425. for (size_t i = 0; i < len; ++i) {
  426. if (buffer[i] == '.') {
  427. buffer[i] = ':';
  428. }
  429. }
  430. std::string collector_name(buffer, len);
  431. #ifdef Py_GIL_DISABLED
  432. ReMutexHolder holder(client->_lock);
  433. #endif
  434. collector_index = client->make_collector_with_relname(refs_collector.get_index(), collector_name).get_index();
  435. client->_python_type_collectors[cls] = collector_index;
  436. }
  437. switch (event) {
  438. case PyRefTracer_CREATE:
  439. client->add_level(collector_index, 0, 1);
  440. break;
  441. case PyRefTracer_DESTROY:
  442. client->add_level(collector_index, 0, -1);
  443. break;
  444. }
  445. return 0;
  446. }
  447. #endif
  448. #endif // HAVE_PYTHON && DO_PSTATS