Skip to content

Commit 72ee7ec

Browse files
Subbarao Garlapatimeta-codesync[bot]
authored andcommitted
Add phase one of debugger/profiler support to the JIT
Summary: Add [phase one](https://fb.workplace.com/groups/pyruntimedevs/permalink/1367828274875246/) of debugger/profiler support to the Cinder JIT. This is done by patching `sys.monitoring.register_callback` since all debugger/profiler enablement/disablement flow through this method. If a callback is passed then we disable the JIT. If a callback is removed and there are no callbacks left, we enable the JIT. Notes: - This feature is off by default and will be enabled in a future Diff (to allow for easy rollback if needed) - `sys.monitoring` was added in 3.12 so this support is only for Python 3.12+ Reviewed By: alexmalyshev Differential Revision: D89371802 fbshipit-source-id: 82767c1699a89c98bdff3affb915b438dcbcd61d
1 parent 0f7c7db commit 72ee7ec

File tree

4 files changed

+491
-13
lines changed

4 files changed

+491
-13
lines changed

cinderx/Jit/config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ struct Config {
153153
// Whether or not to JIT specialized opcodes or to fall back to their generic
154154
// counterparts.
155155
bool specialized_opcodes{false};
156+
// Support sys.monitoring (by falling back to the interpreter).
157+
bool support_monitoring{false};
156158

157159
// Add RefineType instructions for Static Python values before they get
158160
// typechecked. Enabled by default as HIR doesn't pass through Static Python

cinderx/Jit/pyjit.cpp

Lines changed: 169 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
#endif
1111

1212
#include "internal/pycore_pystate.h"
13+
#if PY_VERSION_HEX >= 0x030E0000
14+
#include "internal/pycore_interp_structs.h"
15+
#endif
1316

1417
#include "cinderx/Common/audit.h"
1518
#include "cinderx/Common/code.h"
@@ -761,6 +764,14 @@ FlagProcessor initFlagProcessor() {
761764
getMutableConfig().specialized_opcodes,
762765
"JIT specialized opcodes or to fall back to their generic counterparts.");
763766

767+
#if PY_VERSION_HEX >= 0x030C0000
768+
flag_processor.addOption(
769+
"jit-support-monitoring",
770+
"PYTHONJITSUPPORTMONITORING",
771+
getMutableConfig().support_monitoring,
772+
"Support monitoring (e.g. debugging/profiling)");
773+
#endif
774+
764775
flag_processor.setFlags(PySys_GetXOptions());
765776

766777
// T198250666: Bit of a hack but this makes other things easier. In 3.12 all
@@ -1294,17 +1305,9 @@ bool deoptFunc(BorrowedRef<PyFunctionObject> func) {
12941305
return false;
12951306
}
12961307

1297-
PyObject* disable_jit(PyObject* /* self */, PyObject* args, PyObject* kwargs) {
1298-
int deopt_all = 0;
1299-
1300-
const char* keywords[] = {"deopt_all", nullptr};
1301-
1302-
if (!PyArg_ParseTupleAndKeywords(
1303-
args, kwargs, "|p", const_cast<char**>(keywords), &deopt_all)) {
1304-
return nullptr;
1305-
}
1308+
void disable_jit_impl(bool deopt_all) {
13061309
if (jitCtx() == nullptr) {
1307-
Py_RETURN_NONE;
1310+
return;
13081311
}
13091312

13101313
if (deopt_all) {
@@ -1325,19 +1328,32 @@ PyObject* disable_jit(PyObject* /* self */, PyObject* args, PyObject* kwargs) {
13251328
getMutableConfig().state = State::kPaused;
13261329
JIT_DLOG("Disabled the JIT");
13271330
}
1331+
}
1332+
1333+
PyObject* disable_jit(PyObject* /* self */, PyObject* args, PyObject* kwargs) {
1334+
int deopt_all = 0;
1335+
1336+
const char* keywords[] = {"deopt_all", nullptr};
1337+
1338+
if (!PyArg_ParseTupleAndKeywords(
1339+
args, kwargs, "|p", const_cast<char**>(keywords), &deopt_all)) {
1340+
return nullptr;
1341+
}
1342+
1343+
disable_jit_impl(deopt_all);
13281344

13291345
Py_RETURN_NONE;
13301346
}
13311347

1332-
PyObject* enable_jit(PyObject* /* self */, PyObject* /* arg */) {
1348+
bool enable_jit_impl() {
13331349
if (jitCtx() == nullptr) {
13341350
PyErr_SetString(
13351351
PyExc_RuntimeError,
13361352
"Trying to re-enable the JIT but the JIT context is missing");
1337-
return nullptr;
1353+
return false;
13381354
}
13391355
if (isJitUsable()) {
1340-
Py_RETURN_NONE;
1356+
return true;
13411357
}
13421358

13431359
size_t count = 0;
@@ -1350,9 +1366,69 @@ PyObject* enable_jit(PyObject* /* self */, PyObject* /* arg */) {
13501366

13511367
JIT_DLOG("Re-enabled the JIT and re-optimized {} functions", count);
13521368

1369+
return true;
1370+
}
1371+
1372+
PyObject* enable_jit(PyObject* /* self */, PyObject* /* arg */) {
1373+
if (!enable_jit_impl()) {
1374+
return nullptr;
1375+
}
13531376
Py_RETURN_NONE;
13541377
}
13551378

1379+
#if PY_VERSION_HEX >= 0x030C0000
1380+
1381+
// Check if sys.monitoring has any active callbacks registered.
1382+
bool hasRegisteredMonitoringCallbacks() {
1383+
auto is = PyInterpreterState_Get();
1384+
for (int tool_id = 0; tool_id < PY_MONITORING_TOOL_IDS; ++tool_id) {
1385+
for (int event_id = 0; event_id < _PY_MONITORING_EVENTS; ++event_id) {
1386+
BorrowedRef<> entry = is->monitoring_callables[tool_id][event_id];
1387+
if (entry != nullptr && !Py_IsNone(entry)) {
1388+
return true;
1389+
}
1390+
}
1391+
}
1392+
return false;
1393+
}
1394+
1395+
// Patched version of sys.monitoring.register_callback().
1396+
// Intercepts callback registration/deregistration to disable/enable the JIT
1397+
// This is to handle debuggers/profilers attaching or detaching.
1398+
PyObject* patched_sys_monitoring_register_callback(
1399+
PyObject* /* self */,
1400+
PyObject* const* args,
1401+
Py_ssize_t nargs) {
1402+
auto mod_state = cinderx::getModuleState();
1403+
BorrowedRef<> original =
1404+
mod_state->getOriginalSysMonitoringRegisterCallback();
1405+
JIT_CHECK(
1406+
original != nullptr,
1407+
"Expecting to have sys.monitoring.register_callback already saved");
1408+
1409+
// Run the original function first
1410+
PyObject* result =
1411+
PyObject_Vectorcall(original, args, nargs, nullptr /* kwnames */);
1412+
if (result == nullptr) {
1413+
return nullptr;
1414+
}
1415+
1416+
// Check if we need to enable or disable the JIT based on whether any
1417+
// monitoring callbacks are still registered.
1418+
if (hasRegisteredMonitoringCallbacks()) {
1419+
disable_jit_impl(true /* deopt_all */);
1420+
} else {
1421+
if (!enable_jit_impl()) {
1422+
Py_DECREF(result);
1423+
return nullptr;
1424+
}
1425+
}
1426+
1427+
return result;
1428+
}
1429+
1430+
#endif // PY_VERSION_HEX >= 0x030C0000
1431+
13561432
void compile_after_n_calls_impl(uint32_t calls) {
13571433
getMutableConfig().compile_after_n_calls = calls;
13581434

@@ -2395,6 +2471,70 @@ PyObject* after_fork_child(PyObject*, PyObject*) {
23952471
Py_RETURN_NONE;
23962472
}
23972473

2474+
// Patch sys.monitoring.register_callback to intercept debugger/profiler
2475+
// attachment.
2476+
void patchSysMonitoringFunctions(PyObject* cinderjit_module) {
2477+
#if PY_VERSION_HEX >= 0x030C0000
2478+
BorrowedRef<> monitoring = PySys_GetObject("monitoring");
2479+
if (monitoring == nullptr) {
2480+
JIT_DLOG("sys.monitoring not found, skipping JIT monitoring integration");
2481+
return;
2482+
}
2483+
2484+
Ref<> original =
2485+
Ref<>::steal(PyObject_GetAttrString(monitoring, "register_callback"));
2486+
if (original == nullptr) {
2487+
PyErr_Clear();
2488+
JIT_DLOG(
2489+
"sys.monitoring.register_callback not found, skipping JIT monitoring "
2490+
"integration");
2491+
return;
2492+
}
2493+
2494+
auto mod_state = cinderx::getModuleState();
2495+
mod_state->setOriginalSysMonitoringRegisterCallback(original);
2496+
2497+
Ref<> patched_func = Ref<>::steal(PyObject_GetAttrString(
2498+
cinderjit_module, "patched_sys_monitoring_register_callback"));
2499+
if (patched_func == nullptr) {
2500+
JIT_LOG(
2501+
"Failed to get patched_sys_monitoring_register_callback from cinderjit "
2502+
"module");
2503+
PyErr_Clear();
2504+
return;
2505+
}
2506+
2507+
if (PyObject_SetAttrString(monitoring, "register_callback", patched_func) <
2508+
0) {
2509+
JIT_LOG("Failed to patch sys.monitoring.register_callback");
2510+
PyErr_Clear();
2511+
return;
2512+
}
2513+
2514+
JIT_DLOG("Successfully patched sys.monitoring.register_callback");
2515+
#endif // PY_VERSION_HEX >= 0x030C0000
2516+
}
2517+
2518+
void restoreSysMonitoringRegisterCallback() {
2519+
#if PY_VERSION_HEX >= 0x030C0000
2520+
auto mod_state = cinderx::getModuleState();
2521+
BorrowedRef<> original =
2522+
mod_state->getOriginalSysMonitoringRegisterCallback();
2523+
if (original == nullptr) {
2524+
return;
2525+
}
2526+
2527+
BorrowedRef<> monitoring = PySys_GetObject("monitoring");
2528+
if (monitoring == nullptr) {
2529+
return;
2530+
}
2531+
2532+
if (PyObject_SetAttrString(monitoring, "register_callback", original) < 0) {
2533+
PyErr_Clear();
2534+
}
2535+
#endif // PY_VERSION_HEX >= 0x030C0000
2536+
}
2537+
23982538
PyMethodDef jit_methods[] = {
23992539
{"disable",
24002540
reinterpret_cast<PyCFunction>(disable_jit),
@@ -2408,6 +2548,14 @@ PyMethodDef jit_methods[] = {
24082548
PyDoc_STR(
24092549
"Re-enable the JIT and re-attach compiled onto previously "
24102550
"JIT-compiled functions.")},
2551+
#if PY_VERSION_HEX >= 0x030C0000
2552+
{"patched_sys_monitoring_register_callback",
2553+
_PyCFunction_CAST(patched_sys_monitoring_register_callback),
2554+
METH_FASTCALL,
2555+
PyDoc_STR(
2556+
"Patched version of sys.monitoring.register_callback that "
2557+
"disables/enables the JIT when debuggers/profilers attach/detach.")},
2558+
#endif
24112559
{"auto",
24122560
auto_jit,
24132561
METH_NOARGS,
@@ -3078,6 +3226,12 @@ int initialize() {
30783226
return -1;
30793227
}
30803228

3229+
#if PY_VERSION_HEX >= 0x030C0000
3230+
if (getConfig().support_monitoring) {
3231+
patchSysMonitoringFunctions(mod);
3232+
}
3233+
#endif
3234+
30813235
getMutableConfig().state = State::kRunning;
30823236

30833237
mod_state->setJitList(std::move(jit_list));
@@ -3127,6 +3281,8 @@ void finalize() {
31273281

31283282
g_aot_ctx.destroy();
31293283

3284+
restoreSysMonitoringRegisterCallback();
3285+
31303286
getMutableConfig().state = State::kNotInitialized;
31313287
}
31323288

0 commit comments

Comments
 (0)