Skip to content

Remove CompositeTracer wrapper layer (item 2 of #115)#409

Open
islamicfinanceaaoifi-ops wants to merge 1 commit into
pschanely:mainfrom
islamicfinanceaaoifi-ops:fix/issue-115-remove-composite-tracer
Open

Remove CompositeTracer wrapper layer (item 2 of #115)#409
islamicfinanceaaoifi-ops wants to merge 1 commit into
pschanely:mainfrom
islamicfinanceaaoifi-ops:fix/issue-115-remove-composite-tracer

Conversation

@islamicfinanceaaoifi-ops
Copy link
Copy Markdown

@islamicfinanceaaoifi-ops islamicfinanceaaoifi-ops commented May 12, 2026

This PR addresses item (2) of #115"CompositeTracer is now largely just a wrapper around CTracer. Let's try to remove the CompositeTracer layer entirely."

Item (3) (porting TracingModule to C) is left for a follow-up PR so the diff stays reviewable; happy to take that on next if this approach looks right.

What changed

The old CompositeTracer class owned a self.ctracer = CTracer() instance and forwarded most methods through. The session lifecycle — push/pop the patching_module, wire sys.monitoring (3.12+) or sys.settrace (older), start/stop the tracer — lived in its __enter__/__exit__. This PR folds those responsibilities into CTracer itself and deletes the wrapper.

C side (_tracers.c, _tracers.h)

  • New patching_module slot on CTracer, exposed via tp_getset. Defaults to None.
  • New __enter__ / __exit__ C methods that push the patching_module, configure sys.monitoring on 3.12+ (or stash sys.gettrace() and enable opcode tracing on the current frame for older Python), call start()/stop(), then unwind on exit.
  • New trace_caller C method (no-op on 3.12+; on older Python enables opcode tracing on the calling frame — same as the previous sys._getframe(2)-based Python implementation).
  • CTracer.push_module now invokes sys.monitoring.restart_events() itself on 3.12+ (only when the tracer is enabled — restarting has no effect otherwise). That restart_events() call used to live in the now-deleted CompositeTracer.push_module.
  • Helpers _ch_load_sys_monitoring and _ch_call_monitoring_method centralize the sys.monitoring lookup so the lifecycle methods stay readable.

Python side (tracers.py)

  • CompositeTracer class is gone. The module singleton is now just COMPOSITE_TRACER = CTracer() with patching_module = PatchingModule() set explicitly.
  • is_tracing, NoTracing, ResumedTracing, and PushedModule now go through the CTracer instance directly instead of COMPOSITE_TRACER.ctracer.

Caller renames

The wrapper provided two name-only shims; callers move to the unified CTracer API:

  • COMPOSITE_TRACER.pop_config(m) -> COMPOSITE_TRACER.pop_module(m)
  • COMPOSITE_TRACER.set_postop_callback(callback, frame) -> COMPOSITE_TRACER.push_postop_callback(frame, callback) (argument order swap)

Touches opcode_intercept.py (7 callsites), enforce.py, core.py, tracers_test.py.

Verified locally (Python 3.14.3 / Windows 11 / MSVC 14.50)

pytest crosshair/tracers_test.py crosshair/_tracers_test.py     # 12 / 12 pass (3 skipped baseline)
pytest crosshair/condition_parser_test.py                       # 27 / 27 pass
pytest crosshair/enforce_test.py crosshair/statespace_test.py   # 16 / 16 pass
pytest crosshair/util_test.py crosshair/opcode_intercept_test.py
pytest crosshair/fuzz_core_test.py                              # 1294 / 1294 pass
pytest crosshair/core_test.py crosshair/diff_behavior_test.py crosshair/path_cover_test.py   # 106 / 106 pass

libimpl/ also runs through; the only failure I saw (datetimelib_ch_test::test_builtin[check_datetime_astimezone]) reproduces on unmodified main and isn't related to this refactor — current main CI is red for the same family of path-exploration failures.

Design notes / questions for reviewer

  • I kept the public name COMPOSITE_TRACER so this PR doesn't ripple a rename through downstream consumers. If you'd rather call it TRACER or similar now that it's just a CTracer, happy to follow up.
  • sys.monitoring.restart_events() inside push_module is now guarded by self->enabled. The previous Python wrapper called it unconditionally. Let me know if you'd prefer the unconditional behavior — I went conservative on the perf side since it's only meaningful when the tracer is actively running.
  • _PyFrame_GetBackBorrow in trace_caller walks back one frame to mirror the old sys._getframe(2) semantics (top Python frame on the eval stack is the caller of trace_caller, one back is the function we want to trace).
  • prev_traceobj is stored on the struct for the older-Python branch so __exit__ can restore sys.settrace to whatever was set before the session.

Ready for item (3) (porting TracingModule.trace_op + _CALL_HANDLERS and the handle_call_* family to C) as a follow-up — let me know if you'd like me to open that PR next.


IssueHunt Summary

Referenced issues

This pull request has been submitted to:


The session lifecycle (push/pop the patching_module, wire sys.monitoring
on 3.12+ or sys.settrace on earlier versions, start/stop the tracer) used
to live in a Python `CompositeTracer` class that simply wrapped a
`self.ctracer = CTracer()` and forwarded most methods through. The
maintainer's spec on pschanely#115 calls for removing that layer entirely.

This change folds the wrapper's responsibilities into CTracer in C:

- `CTracer` gains a `patching_module` attribute (tp_getset) so callers no
  longer have to reach through a wrapper object to access it.
- `CTracer.__enter__` / `CTracer.__exit__` are now native methods that
  push the patching_module, configure sys.monitoring (3.12+) or save and
  install via sys.settrace (older), call start/stop, then unwind.
- `CTracer.trace_caller` is implemented in C (no-op on 3.12+; on older
  Python it enables opcode tracing on the calling frame, matching the
  previous Python implementation that used `sys._getframe(2)`).
- The unconditional `sys.monitoring.restart_events()` that used to live
  in `CompositeTracer.push_module` is now invoked from
  `CTracer_push_module` itself (only when the tracer is currently
  enabled, since restarting events has no effect otherwise).
- Helpers `_ch_load_sys_monitoring` and `_ch_call_monitoring_method`
  centralize the sys.monitoring lookup so the lifecycle methods stay
  readable.

`tracers.py`: deletes the `CompositeTracer` class entirely. The module
singleton is now just `COMPOSITE_TRACER = CTracer()` with its
`patching_module` set explicitly. `is_tracing`, `NoTracing`, and
`ResumedTracing` go through the CTracer instance directly instead of
`COMPOSITE_TRACER.ctracer`.

Callers are updated to the unified API:
  - `COMPOSITE_TRACER.pop_config(m)` -> `COMPOSITE_TRACER.pop_module(m)`
  - `COMPOSITE_TRACER.set_postop_callback(cb, frame)` ->
    `COMPOSITE_TRACER.push_postop_callback(frame, cb)` (note arg swap)

Touches: opcode_intercept.py (7 callsites), enforce.py, core.py,
tracers_test.py.

Verified: tracers_test.py, _tracers_test.py, condition_parser_test.py,
enforce_test.py, statespace_test.py, util_test.py, opcode_intercept_test.py,
fuzz_core_test.py, core_test.py, diff_behavior_test.py, path_cover_test.py
all pass on Python 3.14.3 / Windows.
@pschanely
Copy link
Copy Markdown
Owner

@islamicfinanceaaoifi-ops thanks for helping! Please check contributing.rst for getting the formatting/linting etc right.

sys.monitoring.restart_events() inside push_module is now guarded by self->enabled

I expect that we'll still need this to happen because the tracer can be re-enabled later. If the tests pass anyway, I'd want to know whether we need test coverage there, or why my understanding is flawed! :)

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.

3 participants