Skip to content

Fix trace/profile events for generators and exceptions#7268

Merged
youknowone merged 1 commit intoRustPython:mainfrom
youknowone:bdb
Mar 1, 2026
Merged

Fix trace/profile events for generators and exceptions#7268
youknowone merged 1 commit intoRustPython:mainfrom
youknowone:bdb

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Feb 28, 2026

  • Move prev_line from ExecutingFrame to FrameState so it persists across generator/coroutine suspend and resume
  • Add TraceEvent::Exception and fire_exception_trace helper for sys.settrace exception events in execution loop, FOR_ITER, and SEND
  • Add is_profile_event filter so sys.setprofile skips line/exception
  • Fire return trace event on both normal return and exception unwind (PY_UNWIND → PyTrace_RETURN) and propagate trace function errors
  • Extract dispatch_traced_frame helper from with_frame/resume_gen_frame
  • Remove 4 expectedFailure markers in test_bdb (generator tests)
  • Remove 14 expectedFailure markers in test_sys_setprofile

Summary by CodeRabbit

  • New Features

    • Added exception and opcode trace events to improve debugging and profiling.
  • Improvements / Bug Fixes

    • Per-code event masking to prevent cross-code event leakage.
    • More accurate line-number tracking across suspended/resumed frames.
    • Exception trace events now fire when exceptions are raised or resume points produce new exceptions.
  • Refactor

    • Centralized tracing dispatch to unify Call/Return tracing behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Introduces exception and opcode trace events, moves per-frame line tracking into FrameState, adds per-code event masking, centralizes call/return trace dispatch into a helper, and updates frame execution paths to use the new tracing and monitoring flows.

Changes

Cohort / File(s) Summary
Dictionary Maintenance
/.cspell.dict/cpython.txt
Added ceval dictionary entry.
Frame state & tracing logic
crates/vm/src/frame.rs
Moved prev_line into FrameState; persist line across suspensions; added fire_exception_trace() and opcode trace emission; use state.events_for_code() for per-code monitoring; removed prev_line from ExecutingFrame.
Trace protocol & events
crates/vm/src/protocol/callable.rs
Added TraceEvent::Exception and TraceEvent::Opcode; added is_profile_event() and is_opcode_event(); updated Display mapping and trace gating (opcode vs profile events).
Monitoring per-code masking
crates/vm/src/stdlib/sys/monitoring.rs
Added MonitoringState::events_for_code(code_id) to compute per-code event masks and used it when re-instrumenting code.
VM dispatch refactor
crates/vm/src/vm/mod.rs
Removed direct TraceEvent re-export; added dispatch_traced_frame() helper to fire Call/Return and manage per-frame trace state; refactored with_frame_exc() and resume_gen_frame() to use the helper.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant VM
    participant Frame
    participant MonitoringState
    participant TraceFunc

    Caller->>VM: invoke frame execution
    VM->>MonitoringState: events_for_code(code_id)
    VM->>TraceFunc: call TraceFunc(frame, "call", ...)
    TraceFunc-->>VM: optional per-frame trace object
    VM->>Frame: execute body (may yield/raise)
    Frame->>MonitoringState: consult per-frame events (opcode/line)
    alt exception raised
        Frame->>TraceFunc: call TraceFunc(frame, "exception", (type,value,traceback))
    else normal return
        VM->>TraceFunc: call TraceFunc(frame, "return", result)
    end
    VM-->>Caller: return/resume result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ShaharNaveh

Poem

🐰 Hopping lines and tracing fate,

Exceptions logged before it's late,
Per-code masks keep signals neat,
Calls and returns now dance complete,
A rabbit cheers: the VM's in sync!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix trace/profile events for generators and exceptions' directly and clearly summarizes the main changes in the PR, which involve fixing trace and profile event handling across generators, exceptions, and the execution loop.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

Code has been automatically formatted

The code in this PR has been formatted using:

  • cargo fmt --all
    Please pull the latest changes before pushing again:
git pull origin bdb

@github-actions
Copy link
Contributor

github-actions bot commented Feb 28, 2026

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/bdb.py
[x] test: cpython/Lib/test/test_bdb.py

dependencies:

  • bdb

dependent tests: (1 tests)

  • bdb: test_bdb

[x] test: cpython/Lib/test/test_sys.py (TODO: 8)
[ ] test: cpython/Lib/test/test_syslog.py (TODO: 2)
[ ] test: cpython/Lib/test/test_sys_setprofile.py
[ ] test: cpython/Lib/test/test_sys_settrace.py (TODO: 45)

dependencies:

dependent tests: (225 tests)

  • sys: regrtestdata test___all__ test__colorize test__locale test__osx_support test_android test_annotationlib test_argparse test_array test_ast test_asyncio test_audit test_bdb test_bigaddrspace test_bigmem test_bisect test_buffer test_builtin test_bytes test_bz2 test_c_locale_coercion test_calendar test_call test_class test_cmath test_cmd test_cmd_line test_cmd_line_script test_code test_code_module test_codeccallbacks test_codecs test_collections test_compile test_compileall test_complex test_concurrent_futures test_context test_contextlib test_coroutines test_csv test_ctypes test_datetime test_dbm test_dbm_sqlite3 test_decimal test_descr test_dict test_difflib test_dis test_doctest test_doctest2 test_docxmlrpc test_dtrace test_dynamic test_dynamicclassattribute test_email test_ensurepip test_enum test_enumerate test_eof test_except_star test_exceptions test_faulthandler test_fcntl test_file test_file_eintr test_fileinput test_fileio test_float test_fork1 test_format test_fractions test_frozen test_functools test_future_stmt test_gc test_generators test_genericpath test_getopt test_glob test_grammar test_gzip test_hash test_hashlib test_http_cookiejar test_httpservers test_importlib test_inspect test_int test_io test_iter test_itertools test_json test_largefile test_launcher test_list test_listcomps test_locale test_logging test_long test_lzma test_mailbox test_marshal test_math test_memoryio test_memoryview test_metaclass test_mimetypes test_mmap test_monitoring test_msvcrt test_multiprocessing_fork test_multiprocessing_forkserver test_multiprocessing_main_handling test_multiprocessing_spawn test_netrc test_ntpath test_numeric_tower test_operator test_optparse test_ordered_dict test_os test_osx_env test_pathlib test_patma test_peepholer test_pickle test_pkg test_pkgutil test_platform test_plistlib test_popen test_posix test_posixpath test_print test_property test_pty test_pwd test_py_compile test_pyclbr test_pydoc test_pyexpat test_quopri test_raise test_range test_re test_regrtest test_repl test_reprlib test_resource test_runpy test_sax test_scope test_script_helper test_select test_selectors test_shutil test_signal test_site test_slice test_smtplib test_socket test_sqlite3 test_ssl test_stable_abi_ctypes test_stat test_statistics test_str test_strftime test_string_literals test_strtod test_struct test_subprocess test_support test_sys test_sys_setprofile test_sys_settrace test_sysconfig test_syslog test_tarfile test_tempfile test_termios test_threadedtempfile test_threading test_threading_local test_threadsignals test_time test_timeit test_tomllib test_tools test_trace test_traceback test_tuple test_type_comments test_types test_typing test_unicode_file test_unicode_file_functions test_unicodedata test_unittest test_univnewlines test_urllib test_urllib2 test_urllib2net test_urlparse test_utf8_mode test_uuid test_venv test_wait3 test_wait4 test_wave test_weakref test_webbrowser test_winconsoleio test_winreg test_with test_wsgiref test_xml_etree test_xmlrpc test_zipapp test_zipfile test_zipfile64 test_zipimport test_zlib

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone youknowone marked this pull request as ready for review March 1, 2026 02:42
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/frame.rs`:
- Around line 599-612: The opcode trace block fires without checking the frame's
f_trace_opcodes flag; update the conditional that currently checks
vm.use_tracing and self.object.trace.lock() to also require the frame's
trace-opcode flag (e.g. self.object.trace_opcodes or equivalent field on the
frame object) is true before calling vm.trace_event; specifically modify the if
that excludes Instruction::Resume/InstrumentedResume/ExtendedArg so it
additionally checks the frame-level f_trace_opcodes flag and only calls
vm.trace_event(crate::protocol::TraceEvent::Opcode, None)? when that flag is
set.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b05e74b and 0a6c7c0.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_bdb.py is excluded by !Lib/**
  • Lib/test/test_sys_setprofile.py is excluded by !Lib/**
📒 Files selected for processing (5)
  • .cspell.dict/cpython.txt
  • crates/vm/src/frame.rs
  • crates/vm/src/protocol/callable.rs
  • crates/vm/src/stdlib/sys/monitoring.rs
  • crates/vm/src/vm/mod.rs

- Add TraceEvent::Exception and Opcode variants with profile filtering
- Extract dispatch_traced_frame helper for Call/Return trace events
- Fire exception trace on new raises, SEND StopIteration, FOR_ITER StopIteration
- Fire opcode trace events gated by f_trace_opcodes
- Move prev_line to FrameState for persistence across generator suspend/resume
- Reset prev_line in gen_throw for correct LINE monitoring after yield
- Add per-code event mask (events_for_code) to prevent unrelated code instrumentation
- Remove expectedFailure markers from test_bdb (5) and test_sys_setprofile (14)
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/vm/src/frame.rs (1)

2217-2221: Extract duplicated stale-instrumentation refresh logic.

Line 2217 and Line 2597 duplicate the same re-instrumentation/version-sync block. Pulling that into one helper will reduce drift risk between Resume and InstrumentedResume.

♻️ Proposed refactor
+    #[inline]
+    fn refresh_instrumentation_if_stale(&mut self, vm: &VirtualMachine) -> bool {
+        let global_ver = vm
+            .state
+            .instrumentation_version
+            .load(atomic::Ordering::Acquire);
+        let code_ver = self
+            .code
+            .instrumentation_version
+            .load(atomic::Ordering::Acquire);
+        if code_ver != global_ver {
+            let events = {
+                let state = vm.state.monitoring.lock();
+                state.events_for_code(self.code.get_id())
+            };
+            monitoring::instrument_code(self.code, events);
+            self.code
+                .instrumentation_version
+                .store(global_ver, atomic::Ordering::Release);
+            self.update_lasti(|i| *i -= 1);
+            return true;
+        }
+        false
+    }
-                if code_ver != global_ver {
-                    let events = {
-                        let state = vm.state.monitoring.lock();
-                        state.events_for_code(self.code.get_id())
-                    };
-                    monitoring::instrument_code(self.code, events);
-                    self.code
-                        .instrumentation_version
-                        .store(global_ver, atomic::Ordering::Release);
-                    // Re-execute this instruction (it may now be INSTRUMENTED_RESUME)
-                    self.update_lasti(|i| *i -= 1);
-                }
+                if self.refresh_instrumentation_if_stale(vm) {
+                    // Re-execute this instruction (it may now be INSTRUMENTED_RESUME)
+                }
-                if code_ver != global_ver {
-                    let events = {
-                        let state = vm.state.monitoring.lock();
-                        state.events_for_code(self.code.get_id())
-                    };
-                    monitoring::instrument_code(self.code, events);
-                    self.code
-                        .instrumentation_version
-                        .store(global_ver, atomic::Ordering::Release);
-                    // Re-execute (may have been de-instrumented to base Resume)
-                    self.update_lasti(|i| *i -= 1);
-                    return Ok(None);
-                }
+                if self.refresh_instrumentation_if_stale(vm) {
+                    // Re-execute (may have been de-instrumented to base Resume)
+                    return Ok(None);
+                }

As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code."

Also applies to: 2597-2600

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/frame.rs` around lines 2217 - 2221, The duplicated
re-instrumentation logic in Resume and InstrumentedResume (the block that locks
vm.state.monitoring, calls state.events_for_code(self.code.get_id()), then calls
monitoring::instrument_code(self.code, events)) should be extracted into a
single helper (e.g., refresh_instrumentation or sync_instrumentation_for_code)
to avoid drift; create a private function that takes &self (or &Code) and
&VmState/monitoring handle as needed, move the lock+events_for_code call and the
monitoring::instrument_code call into it, and replace the duplicated blocks in
Resume and InstrumentedResume with a single call to this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/vm/src/vm/mod.rs`:
- Around line 1163-1173: The return trace currently always passes None to
self.trace_event for TraceEvent::Return; instead, inspect the function outcome
(the local variable result that holds Ok(R) or Err): when result is Ok(value)
extract that return value and pass it as the arg to self.trace_event (so
trace_event receives the actual return object), and when result is Err pass None
as before; update the call site around the existing self.use_tracing check
(references: self.use_tracing.get(), frame.trace.lock(),
self.profile_func.borrow(), trace_event / TraceEvent::Return, ret_result) to
build the correct arg from result before calling trace_event and then propagate
ret_result? using the same error-handling pattern (ret_result?).

---

Nitpick comments:
In `@crates/vm/src/frame.rs`:
- Around line 2217-2221: The duplicated re-instrumentation logic in Resume and
InstrumentedResume (the block that locks vm.state.monitoring, calls
state.events_for_code(self.code.get_id()), then calls
monitoring::instrument_code(self.code, events)) should be extracted into a
single helper (e.g., refresh_instrumentation or sync_instrumentation_for_code)
to avoid drift; create a private function that takes &self (or &Code) and
&VmState/monitoring handle as needed, move the lock+events_for_code call and the
monitoring::instrument_code call into it, and replace the duplicated blocks in
Resume and InstrumentedResume with a single call to this helper.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6c7c0 and dbd7210.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_bdb.py is excluded by !Lib/**
  • Lib/test/test_sys_setprofile.py is excluded by !Lib/**
📒 Files selected for processing (5)
  • .cspell.dict/cpython.txt
  • crates/vm/src/frame.rs
  • crates/vm/src/protocol/callable.rs
  • crates/vm/src/stdlib/sys/monitoring.rs
  • crates/vm/src/vm/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • .cspell.dict/cpython.txt

Comment on lines +1163 to +1173
// Fire 'return' event if frame is being traced or profiled.
// PY_UNWIND fires PyTrace_RETURN with arg=None — so we fire for
// both Ok and Err, matching `call_trace_protected` behavior.
if self.use_tracing.get()
&& (!self.is_none(&frame.trace.lock()) || !self.is_none(&self.profile_func.borrow()))
{
let ret_result = self.trace_event(TraceEvent::Return, None);
// call_trace_protected: if trace function raises, its error
// replaces the original exception.
ret_result?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

CPython sys.settrace protocol return event arg value successful return exception

💡 Result:

In CPython, a sys.settrace callback is called as tracefunc(frame, event, arg) where arg depends on event:

  • event == "return": arg is the value the frame is returning, or None if the frame is being exited because an exception is being raised. Also, the trace function’s return value is ignored for "return" events. [1]
  • event == "exception": arg is a 3-tuple (exception, value, traceback), and the trace function’s return value does control the subsequent local trace function. [1]
    (You may see "exception" events at multiple stack levels as the exception propagates.) [1]

How to tell “successful return” vs “exceptional exit”

Because "return" uses arg == None both for (a) a real return None and (b) exiting due to an exception, you generally distinguish them by tracking whether you previously received an "exception" event for that same frame before its "return" event. The docs explicitly state the "return"-arg rule but do not provide a separate “success/failure” flag. [1]

Source
[1] Python sys.settrace documentation (event meanings and arg values). (docs.python.org)

Citations:


🏁 Script executed:

# First, let's examine the code at the specified location
cat -n crates/vm/src/vm/mod.rs | sed -n '1150,1180p'

Repository: RustPython/RustPython

Length of output: 1452


🏁 Script executed:

# Let's also check the TraceEvent type definition and all usages of TraceEvent::Return
rg "TraceEvent::" -A 3 -B 3 --max-count=20

Repository: RustPython/RustPython

Length of output: 4802


🏁 Script executed:

# Check the dispatch_traced_frame function to understand the full context
rg "dispatch_traced_frame" -A 25 -B 5

Repository: RustPython/RustPython

Length of output: 4903


Return trace event must pass the actual return value on successful function return, not always None.

According to CPython's sys.settrace protocol, the "return" event receives arg as either the returned value (on normal return) or None (if exiting due to exception). The current code at line 1169 always passes None regardless of success or failure.

This breaks compatibility with debuggers and profilers that expect to inspect the return value. Fix by extracting the return value from successful results (when result is Ok(R)) and passing it to trace_event, while reserving None for exception cases (when result is Err).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/vm/mod.rs` around lines 1163 - 1173, The return trace currently
always passes None to self.trace_event for TraceEvent::Return; instead, inspect
the function outcome (the local variable result that holds Ok(R) or Err): when
result is Ok(value) extract that return value and pass it as the arg to
self.trace_event (so trace_event receives the actual return object), and when
result is Err pass None as before; update the call site around the existing
self.use_tracing check (references: self.use_tracing.get(), frame.trace.lock(),
self.profile_func.borrow(), trace_event / TraceEvent::Return, ret_result) to
build the correct arg from result before calling trace_event and then propagate
ret_result? using the same error-handling pattern (ret_result?).

@youknowone youknowone merged commit dd6cf74 into RustPython:main Mar 1, 2026
14 checks passed
@youknowone youknowone deleted the bdb branch March 1, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant