Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion ports/webassembly/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async function runCLI() {
}
}

if (!process.stdin.isTTY) {
if (!process.stdin.isTTY && repl) {
contents = fs.readFileSync(0, "utf8");
repl = false;
}
Expand Down
4 changes: 4 additions & 0 deletions py/mpstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ typedef struct _mp_state_vm_t {
uint8_t sched_idx;
#endif

#if MICROPY_PY_SYS_SETTRACE_DUAL_VM
bool vm_switch_pending;
#endif

#if MICROPY_ENABLE_VM_ABORT
bool vm_abort;
nlr_buf_t *nlr_abort;
Expand Down
22 changes: 22 additions & 0 deletions py/profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ static mp_obj_t mp_prof_callback_invoke(mp_obj_t callback, prof_callback_args_t
}

mp_obj_t mp_prof_settrace(mp_obj_t callback) {
#if MICROPY_PY_SYS_SETTRACE_DUAL_VM
bool was_tracing = prof_trace_cb != MP_OBJ_NULL;
#endif
if (mp_obj_is_callable(callback)) {
#if MICROPY_PY_SYS_SETTRACE_DUAL_VM
if (prof_trace_cb == MP_OBJ_NULL) {
Expand All @@ -193,6 +196,25 @@ mp_obj_t mp_prof_settrace(mp_obj_t callback) {
} else {
prof_trace_cb = MP_OBJ_NULL;
}
#if MICROPY_PY_SYS_SETTRACE_DUAL_VM && MICROPY_ENABLE_SCHEDULER
// Signal the standard VM to switch to the tracing VM at the next
// branch point. Only needed when enabling tracing from the disabled
// state; the reverse direction (tracing->standard) is handled by
// the tracing VM's hot-path check at pending_exception_check.
//
// Unlike mp_sched_exception's sched_state bump (which is a
// non-threaded optimisation), this bump is required for correctness:
// it causes the standard VM to enter the slow-path block where the
// VM switch check lives. The vm_switch_pending flag is consumed by
// mp_sched_unlock() in scheduler.c, which keeps sched_state at
// PENDING until the standard VM actually performs the switch.
if (!was_tracing && prof_trace_cb != MP_OBJ_NULL) {
MP_STATE_VM(vm_switch_pending) = true;
if (MP_STATE_VM(sched_state) == MP_SCHED_IDLE) {
MP_STATE_VM(sched_state) = MP_SCHED_PENDING;
}
}
#endif
return mp_const_none;
}

Expand Down
1 change: 1 addition & 0 deletions py/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ typedef enum {
MP_VM_RETURN_NORMAL,
MP_VM_RETURN_YIELD,
MP_VM_RETURN_EXCEPTION,
MP_VM_RETURN_SWITCH_VM,
} mp_vm_return_kind_t;

typedef enum {
Expand Down
3 changes: 3 additions & 0 deletions py/scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ void mp_sched_unlock(void) {
#if MICROPY_SCHEDULER_STATIC_NODES
MP_STATE_VM(sched_head) != NULL ||
#endif
#if MICROPY_PY_SYS_SETTRACE_DUAL_VM
MP_STATE_VM(vm_switch_pending) ||
#endif
mp_sched_num_pending()) {
MP_STATE_VM(sched_state) = MP_SCHED_PENDING;
} else {
Expand Down
34 changes: 34 additions & 0 deletions py/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ MP_NOINLINE static mp_obj_t *build_slice_stack_allocated(byte op, mp_obj_t *sp,
// MP_VM_RETURN_NORMAL, sp valid, return value in *sp
// MP_VM_RETURN_YIELD, ip, sp valid, yielded value in *sp
// MP_VM_RETURN_EXCEPTION, exception in state[0]
// MP_VM_RETURN_SWITCH_VM, ip, sp valid, caller re-invokes via other VM copy
#if MICROPY_VM_IS_STATIC
static
#endif
Expand Down Expand Up @@ -1393,6 +1394,21 @@ unwind_jump:;
// occur every few instructions.
MICROPY_VM_HOOK_LOOP

#if MICROPY_PY_SYS_SETTRACE_DUAL_VM && MICROPY_PY_SYS_SETTRACE
// Tracing VM: check if settrace(None) was called. On the
// tracing VM's hot path; acceptable since tracing already
// has per-instruction overhead via TRACE_TICK.
if (MP_STATE_THREAD(prof_trace_callback) == MP_OBJ_NULL) {
MP_STATE_VM(vm_switch_pending) = false;
nlr_pop();
code_state->ip = ip;
code_state->sp = sp;
code_state->exc_sp_idx = MP_CODE_STATE_EXC_SP_IDX_FROM_PTR(exc_stack, exc_sp);
FRAME_LEAVE();
return MP_VM_RETURN_SWITCH_VM;
}
#endif

// Check for pending exceptions or scheduled tasks to run.
// Note: it's safe to just call mp_handle_pending(true), but
// we can inline the check for the common case where there is
Expand All @@ -1415,6 +1431,24 @@ unwind_jump:;
|| MP_STATE_VM(vm_abort)
#endif
) {
#if MICROPY_PY_SYS_SETTRACE_DUAL_VM && !MICROPY_PY_SYS_SETTRACE
// Standard VM: check if settrace() was called. Only
// reached when the slow path triggers (settrace() bumps
// sched_state), giving zero overhead on the hot path.
// With GIL threading the calling thread holds the GIL
// and reaches here before any other thread.
// Pending exceptions/scheduler callbacks are deferred to
// the tracing VM's next branch point (immediate re-entry).
if (MP_STATE_THREAD(prof_trace_callback) != MP_OBJ_NULL) {
MP_STATE_VM(vm_switch_pending) = false;
nlr_pop();
code_state->ip = ip;
code_state->sp = sp;
code_state->exc_sp_idx = MP_CODE_STATE_EXC_SP_IDX_FROM_PTR(exc_stack, exc_sp);
FRAME_LEAVE();
return MP_VM_RETURN_SWITCH_VM;
}
#endif
MARK_EXC_IP_SELECTIVE();
mp_handle_pending(true);
}
Expand Down
21 changes: 16 additions & 5 deletions py/vm_outer.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,22 @@ static mp_vm_return_kind_t mp_execute_bytecode_standard(mp_code_state_t *code_st
static mp_vm_return_kind_t mp_execute_bytecode_tracing(mp_code_state_t *code_state, volatile mp_obj_t inject_exc);

mp_vm_return_kind_t MICROPY_WRAP_MP_EXECUTE_BYTECODE(mp_execute_bytecode)(mp_code_state_t * code_state, volatile mp_obj_t inject_exc) {
// Select the VM based on whether there is a tracing function installed or not.
if (MP_STATE_THREAD(prof_trace_callback) == MP_OBJ_NULL) {
return mp_execute_bytecode_standard(code_state, inject_exc);
} else {
return mp_execute_bytecode_tracing(code_state, inject_exc);
for (;;) {
mp_vm_return_kind_t ret;
// Select the VM based on whether there is a tracing function installed or not.
// Also use the tracing VM when a trace callback is executing, to avoid
// the standard VM's assert(!mp_prof_is_executing) in FRAME_ENTER.
if (MP_STATE_THREAD(prof_trace_callback) == MP_OBJ_NULL
&& !MP_STATE_THREAD(prof_callback_is_executing)) {
ret = mp_execute_bytecode_standard(code_state, inject_exc);
} else {
ret = mp_execute_bytecode_tracing(code_state, inject_exc);
}
if (ret != MP_VM_RETURN_SWITCH_VM) {
return ret;
}
// On VM switch, clear inject_exc so re-entry doesn't re-inject.
inject_exc = MP_OBJ_NULL;
}
}

Expand Down
207 changes: 207 additions & 0 deletions tests/misc/sys_settrace_midfunction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# test sys.settrace activating/deactivating mid-function (dual-VM branch-point switch)

import sys

try:
sys.settrace
except AttributeError:
print("SKIP")
raise SystemExit


# --- helpers ---

events = []


def make_trace(tag):
def trace(frame, event, arg):
name = frame.f_code.co_name
if not name.startswith("_") and name != "make_trace":
events.append((tag, name, event))
return trace

return trace


def print_events(label):
print(label)
for ev in events:
print(ev)
events.clear()


# --- test 1: enable mid-loop, verify line events fire in same function ---


def test_enable_midloop():
total = 0
for i in range(6):
if i == 3:
sys.settrace(make_trace("T1"))
total += i
sys.settrace(None)
return total


result = test_enable_midloop()
print(result)
print_events("test_enable_midloop")


# --- test 2: disable mid-loop (tracing->standard switch) ---


def test_disable_midloop():
total = 0
for i in range(6):
if i == 3:
sys.settrace(None)
total += i
return total


sys.settrace(make_trace("T2"))
result = test_disable_midloop()
# settrace(None) was called inside, but ensure it's off
sys.settrace(None)
print(result)
print_events("test_disable_midloop")


# --- test 3: nested call gets traced after mid-function enable ---


def helper_add(x):
return x + 10


def test_nested_after_branch():
total = 0
for i in range(3):
if i == 1:
sys.settrace(make_trace("T3"))
total += helper_add(i)
sys.settrace(None)
return total


result = test_nested_after_branch()
print(result)
print_events("test_nested_after_branch")


# --- test 4: toggle on/off/on using loop iterations as branch points ---


def test_toggle():
total = 0
for i in range(9):
if i == 2:
sys.settrace(make_trace("T4"))
elif i == 5:
sys.settrace(None)
elif i == 7:
sys.settrace(make_trace("T4"))
total += i
sys.settrace(None)
return total


result = test_toggle()
print(result)
print_events("test_toggle")


# --- test 5: settrace active through exception handling ---


def test_exception():
total = 0
for i in range(3):
if i == 1:
sys.settrace(make_trace("T5"))
total += i
try:
raise ValueError("test")
except ValueError:
total += 100
sys.settrace(None)
return total


result = test_exception()
print(result)
print_events("test_exception")


# --- test 6: return value integrity through multiple switches ---


def test_return_value():
values = []
for i in range(6):
if i == 2:
sys.settrace(make_trace("T6"))
elif i == 4:
sys.settrace(None)
values.append(i * i)
sys.settrace(None)
return values


result = test_return_value()
print(result)
print_events("test_return_value")


# --- test 7: generator mid-iteration VM switch ---


def gen_counter(n):
for i in range(n):
yield i * 10


def test_generator_midswitch():
g = gen_counter(5)
results = []
results.append(next(g)) # no tracing
results.append(next(g)) # no tracing
sys.settrace(make_trace("T7"))
results.append(next(g)) # tracing starts
results.append(next(g))
results.append(next(g))
sys.settrace(None)
return results


result = test_generator_midswitch()
print(result)
print_events("test_generator_midswitch")


# --- test 8: trace callback disables itself ---


def self_disabling_trace(frame, event, arg):
name = frame.f_code.co_name
if not name.startswith("_") and name != "make_trace":
events.append(("T8", name, event))
if event == "line" and name == "test_self_disable":
sys.settrace(None)
return self_disabling_trace


def test_self_disable():
total = 0
for i in range(4):
total += i
return total


sys.settrace(self_disabling_trace)
result = test_self_disable()
sys.settrace(None)
print(result)
print_events("test_self_disable")
Loading
Loading