Hi!
I am new here, please bear with me. The feedback I am looking for:
- Are there existing ways to solve the problem I am trying to solve.
- If not, is the proposed solution looks reasonable/feasible to you.
The problem I am trying to solve:
I am installing a frame eval function using _PyInterpreterState_SetEvalFrameFunc for the purpose of getting proper runtime stacks for mixed Python/C code for gdb/perf profiler/etc. Eval function calls a generated/jitted trampoline that calls _PyEval_EvalFrameDefault. As the result the native stack contains the generated/jitted trampoline per Python function.
The problem is that this can increase native stack consumption proportionally to the Python stack depth. As far I understand by default the runtime now loops inside of the interpreter w/o consuming native stack space even if Python program has thousands/millions of stack frames.
I want to install the callback by default for a large codebase, or preload to unknown programs w/o getting stack overflow crashes here and there.
Proposed solution:
Let _PyInterpreterState_SetEvalFrameFunc return some special value that will make the interpreter return to the inline loop evaluation w/o adding new native stack frames.
I am not sure if there is possible special value to return from the callback b/c the signature is:
PyObject* EvaluateFrame(PyThreadState* tstate, _PyInterpreterFrame* frame, int exc);
So we would need some special PyObject* value that would mean “the frame wasn’t actually evaluated, fallback to default inline evaluation”.
If the special value is not possible, then we could add another callback that would disable use of the interp->eval_frame. I see that ceval_macros.h now has this define that checks if the eval_frame callback should be used:
#define IS_PEP523_HOOKED(tstate) (tstate->interp->eval_frame != NULL)
So the idea is to allow to install another callback and extend the macro along the lines of:
#define IS_PEP523_HOOKED(tstate, frame) (tstate->interp->eval_frame != NULL && (tstate->interp->use_eval_frame == NULL || tstate->interp->use_eval_frame(tstate, frame)))
If the use_eval_frame callback is installed and returns false, then runtime proceeds to the default inline interpreter evaluation (ignores interp->eval_frame).
In both of these cases hooks can check either current Python stack depth, or remaining native stack space and stop consuming more native stack space if we are dangerously close to crashing.
Note that [tail] calling _PyEval_EvalFrameDefault directly from the frame eval function does not help to stop native stack consumption, since there are still some stack frames per Python function call.
Thanks