Visitar URL original
Refine specialization caches and extend binary-op coverage by youknowone · Pull Request #7386 · RustPython/RustPython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Extract datastack_frame_size_bytes_for_code, skip monitoring for init…
…_cleanup frames, guard trace dispatch

- Extract datastack_frame_size_bytes_for_code as free function, use it
  to compute init_cleanup stack bytes instead of hardcoded constant
- Add monitoring_disabled_for_code to skip instrumentation for
  synthetic init_cleanup code object in RESUME and execute_instrumented
- Add is_trace_event guard so profile-only events skip trace_func dispatch
- Reformat core.rs (rustfmt)
  • Loading branch information
youknowone committed Mar 10, 2026
commit a277b86cfbe3952c515c5f05fca0be1a9c5f9fbb
31 changes: 17 additions & 14 deletions crates/vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,20 +673,7 @@ impl Py<PyFunction> {
/// Returns `None` for generator/coroutine code paths that do not push a
/// regular datastack-backed frame in the fast call path.
pub(crate) fn datastack_frame_size_bytes(&self) -> Option<usize> {
let code: &Py<PyCode> = &self.code;
if code
.flags
.intersects(bytecode::CodeFlags::GENERATOR | bytecode::CodeFlags::COROUTINE)
{
return None;
}
let nlocalsplus = code
.varnames
.len()
.checked_add(code.cellvars.len())?
.checked_add(code.freevars.len())?;
let capacity = nlocalsplus.checked_add(code.max_stackdepth as usize)?;
capacity.checked_mul(core::mem::size_of::<usize>())
datastack_frame_size_bytes_for_code(&self.code)
}

pub(crate) fn prepare_exact_args_frame(
Expand Down Expand Up @@ -782,6 +769,22 @@ impl Py<PyFunction> {
}
}

pub(crate) fn datastack_frame_size_bytes_for_code(code: &Py<PyCode>) -> Option<usize> {
if code
.flags
.intersects(bytecode::CodeFlags::GENERATOR | bytecode::CodeFlags::COROUTINE)
{
return None;
}
let nlocalsplus = code
.varnames
.len()
.checked_add(code.cellvars.len())?
.checked_add(code.freevars.len())?;
let capacity = nlocalsplus.checked_add(code.max_stackdepth as usize)?;
capacity.checked_mul(core::mem::size_of::<usize>())
}
Comment on lines +772 to +786

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the runtime closure length when sizing datastack frames.

Frame::new() sizes localsplus with closure.len(), but this helper hard-codes code.freevars.len(). Since set___code__ still allows swapping in a code object without revalidating the stored closure, callers that reserve datastack space via this helper can compute a smaller layout than prepare_exact_args_frame() will actually allocate.

Possible direction
 pub(crate) fn datastack_frame_size_bytes(&self) -> Option<usize> {
-    datastack_frame_size_bytes_for_code(&self.code)
+    datastack_frame_size_bytes_for_layout(
+        &self.code,
+        self.closure.as_ref().map_or(0, |c| c.len()),
+    )
 }
 
-pub(crate) fn datastack_frame_size_bytes_for_code(code: &Py<PyCode>) -> Option<usize> {
+pub(crate) fn datastack_frame_size_bytes_for_layout(
+    code: &Py<PyCode>,
+    nfrees: usize,
+) -> Option<usize> {
     if code
         .flags
         .intersects(bytecode::CodeFlags::GENERATOR | bytecode::CodeFlags::COROUTINE)
     {
         return None;
     }
     let nlocalsplus = code
         .varnames
         .len()
         .checked_add(code.cellvars.len())?
-        .checked_add(code.freevars.len())?;
+        .checked_add(nfrees)?;
     let capacity = nlocalsplus.checked_add(code.max_stackdepth as usize)?;
     capacity.checked_mul(core::mem::size_of::<usize>())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/builtins/function.rs` around lines 772 - 786, The helper
datastack_frame_size_bytes_for_code currently uses code.freevars.len() which can
differ from the actual runtime closure length used by Frame::new(), leading to
undersized reservations; change the function to take the runtime closure length
(e.g., add a closure_len: usize parameter or accept the closure slice) and use
that value instead of code.freevars.len(), keep the generator/coroutine
early-return behavior, and update all callers (including
prepare_exact_args_frame and any code that reserves datastack space) to pass the
actual closure.len() so sizing matches Frame::new().


impl PyPayload for PyFunction {
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
Expand Down
53 changes: 39 additions & 14 deletions crates/vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use crate::{
builtin_func::PyNativeFunction,
descriptor::{MemberGetter, PyMemberDescriptor, PyMethodDescriptor},
frame::stack_analysis,
function::{PyBoundMethod, PyCell, PyCellRef, PyFunction, vectorcall_function},
function::{
PyBoundMethod, PyCell, PyCellRef, PyFunction, datastack_frame_size_bytes_for_code,
vectorcall_function,
},
list::PyListIterator,
range::PyRangeIterator,
tuple::{PyTuple, PyTupleIterator, PyTupleRef},
Expand Down Expand Up @@ -1259,6 +1262,11 @@ impl fmt::Debug for ExecutingFrame<'_> {
}

impl ExecutingFrame<'_> {
#[inline]
fn monitoring_disabled_for_code(&self, vm: &VirtualMachine) -> bool {
self.code.is(&vm.ctx.init_cleanup_code)
}

fn specialization_new_init_cleanup_frame(&self, vm: &VirtualMachine) -> FrameRef {
Frame::new(
vm.ctx.init_cleanup_code.clone(),
Expand Down Expand Up @@ -3146,6 +3154,17 @@ impl ExecutingFrame<'_> {
self.code.instructions.quicken();
atomic::fence(atomic::Ordering::Release);
}
if self.monitoring_disabled_for_code(vm) {
let global_ver = vm
.state
.instrumentation_version
.load(atomic::Ordering::Acquire);
monitoring::instrument_code(self.code, 0);
self.code
.instrumentation_version
.store(global_ver, atomic::Ordering::Release);
return Ok(None);
}
// Check if bytecode needs re-instrumentation
let global_ver = vm
.state
Expand Down Expand Up @@ -4757,22 +4776,16 @@ impl ExecutingFrame<'_> {
&& let Some(init_func) = cls.get_cached_init_for_specialization(cached_version)
&& let Some(cls_alloc) = cls.slots.alloc.load()
{
// CPython guard is:
// code->co_framesize + _Py_InitCleanup.co_framesize
// and _Py_InitCleanup.co_framesize is defined as:
// 2 + FRAME_SPECIALS_SIZE
// (see cpython/Python/specialize.c).
//
// RustPython datastack stores localsplus payload only, but we
// keep the guard conservative and CPython-shaped by accounting
// for the full cleanup frame size.
const CPYTHON_INIT_CLEANUP_CO_FRAMESIZE_SLOTS: usize = 11;
const INIT_CLEANUP_STACK_BYTES: usize =
CPYTHON_INIT_CLEANUP_CO_FRAMESIZE_SLOTS * core::mem::size_of::<usize>();
// Match CPython's `code->co_framesize + _Py_InitCleanup.co_framesize`
// shape, using RustPython's datastack-backed frame size
// equivalent for the extra shim frame.
let init_cleanup_stack_bytes =
datastack_frame_size_bytes_for_code(&vm.ctx.init_cleanup_code)
.expect("_Py_InitCleanup shim is not a generator/coroutine");
if !self.specialization_has_datastack_space_for_func_with_extra(
vm,
&init_func,
INIT_CLEANUP_STACK_BYTES,
init_cleanup_stack_bytes,
) {
return self.execute_call_vectorcall(nargs, vm);
}
Expand Down Expand Up @@ -5586,6 +5599,18 @@ impl ExecutingFrame<'_> {
instruction.is_instrumented(),
"execute_instrumented called with non-instrumented opcode {instruction:?}"
);
if self.monitoring_disabled_for_code(vm) {
let global_ver = vm
.state
.instrumentation_version
.load(atomic::Ordering::Acquire);
monitoring::instrument_code(self.code, 0);
self.code
.instrumentation_version
.store(global_ver, atomic::Ordering::Release);
self.update_lasti(|i| *i -= 1);
return Ok(None);
}
self.monitoring_mask = vm.state.monitoring_events.load();
match instruction {
Instruction::InstrumentedResume => {
Expand Down
11 changes: 10 additions & 1 deletion crates/vm/src/protocol/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ pub(crate) enum TraceEvent {
}

impl TraceEvent {
/// Whether sys.settrace receives this event.
fn is_trace_event(&self) -> bool {
matches!(
self,
Self::Call | Self::Return | Self::Exception | Self::Line | Self::Opcode
)
}

/// Whether sys.setprofile receives this event.
/// In legacy_tracing.c, profile callbacks are only registered for
/// PY_RETURN, PY_UNWIND, C_CALL, C_RETURN, C_RAISE.
Expand Down Expand Up @@ -211,6 +219,7 @@ impl VirtualMachine {
return Ok(None);
}

let is_trace_event = event.is_trace_event();
let is_profile_event = event.is_profile_event();
let is_opcode_event = event.is_opcode_event();

Expand All @@ -231,7 +240,7 @@ impl VirtualMachine {

// temporarily disable tracing, during the call to the
// tracing function itself.
if !self.is_none(&trace_func) {
if is_trace_event && !self.is_none(&trace_func) {
self.use_tracing.set(false);
let res = trace_func.call(args.clone(), self);
self.use_tracing.set(true);
Expand Down