diff --git a/ports/webassembly/api.js b/ports/webassembly/api.js index 68c19434f2c23..10512ceeb41e7 100644 --- a/ports/webassembly/api.js +++ b/ports/webassembly/api.js @@ -215,7 +215,7 @@ async function runCLI() { } } - if (!process.stdin.isTTY) { + if (!process.stdin.isTTY && repl) { contents = fs.readFileSync(0, "utf8"); repl = false; } diff --git a/py/mpstate.h b/py/mpstate.h index 325c12217521f..74ac678821b67 100644 --- a/py/mpstate.h +++ b/py/mpstate.h @@ -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; diff --git a/py/profile.c b/py/profile.c index 62ed32c1a23d6..dcef4dde130d6 100644 --- a/py/profile.c +++ b/py/profile.c @@ -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) { @@ -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; } diff --git a/py/runtime.h b/py/runtime.h index 0ef811c1e408c..2cd581c884364 100644 --- a/py/runtime.h +++ b/py/runtime.h @@ -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 { diff --git a/py/scheduler.c b/py/scheduler.c index 5355074a7e76f..19baf45c69943 100644 --- a/py/scheduler.c +++ b/py/scheduler.c @@ -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 { diff --git a/py/vm.c b/py/vm.c index eb073cdfad58f..3e38b709fc934 100644 --- a/py/vm.c +++ b/py/vm.c @@ -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 @@ -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 @@ -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); } diff --git a/py/vm_outer.c b/py/vm_outer.c index 6b27f67b189aa..1201993331f53 100644 --- a/py/vm_outer.c +++ b/py/vm_outer.c @@ -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; } } diff --git a/tests/misc/sys_settrace_midfunction.py b/tests/misc/sys_settrace_midfunction.py new file mode 100644 index 0000000000000..b4d850402c0f6 --- /dev/null +++ b/tests/misc/sys_settrace_midfunction.py @@ -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") diff --git a/tests/misc/sys_settrace_midfunction.py.exp b/tests/misc/sys_settrace_midfunction.py.exp new file mode 100644 index 0000000000000..cc724b293c621 --- /dev/null +++ b/tests/misc/sys_settrace_midfunction.py.exp @@ -0,0 +1,122 @@ +15 +test_enable_midloop +('T1', 'test_enable_midloop', 'call') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'line') +('T1', 'test_enable_midloop', 'return') +15 +test_disable_midloop +('T2', 'test_disable_midloop', 'call') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +('T2', 'test_disable_midloop', 'line') +33 +test_nested_after_branch +('T3', 'helper_add', 'call') +('T3', 'helper_add', 'line') +('T3', 'helper_add', 'return') +('T3', 'test_nested_after_branch', 'call') +('T3', 'test_nested_after_branch', 'line') +('T3', 'test_nested_after_branch', 'line') +('T3', 'test_nested_after_branch', 'line') +('T3', 'helper_add', 'call') +('T3', 'helper_add', 'line') +('T3', 'helper_add', 'return') +('T3', 'test_nested_after_branch', 'line') +('T3', 'test_nested_after_branch', 'line') +('T3', 'test_nested_after_branch', 'return') +36 +test_toggle +('T4', 'test_toggle', 'call') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'call') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'line') +('T4', 'test_toggle', 'return') +103 +test_exception +('T5', 'test_exception', 'call') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'exception') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'line') +('T5', 'test_exception', 'return') +[0, 1, 4, 9, 16, 25] +test_return_value +('T6', 'test_return_value', 'call') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +('T6', 'test_return_value', 'line') +[0, 10, 20, 30, 40] +test_generator_midswitch +('T7', 'gen_counter', 'call') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'return') +('T7', 'gen_counter', 'call') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'return') +('T7', 'gen_counter', 'call') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'line') +('T7', 'gen_counter', 'return') +6 +test_self_disable +('T8', 'test_self_disable', 'call') +('T8', 'test_self_disable', 'line') +('T8', 'test_self_disable', 'line') diff --git a/tests/run-tests.py b/tests/run-tests.py index aeb3ba3d056d0..fef4eb295bbfe 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -172,6 +172,7 @@ def open(self, path, mode): "misc/sys_settrace_features.py", "misc/sys_settrace_generator.py", "misc/sys_settrace_loop.py", + "misc/sys_settrace_midfunction.py", # These are bytecode-specific tests. "stress/bytecode_limit.py", ),