Add function tracing to SnapshotTracer#1366
Conversation
Coverage Report for CI Build 29204754231Coverage remained the same at 90.931%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
| self._origin_file = os.path.normcase( | ||
| os.path.abspath(func_frame.f_globals.get("__file__", "")) | ||
| ) | ||
| sys.settrace(self._trace_func) |
There was a problem hiding this comment.
I'm not sure why this is necessary; is it not sufficient to have _trace_func return itself when a new function is called?
There was a problem hiding this comment.
We still need to set self._trace_func as the argument to register it as the global trace function so that on any “call” events in the future (when a function is called inside the with block), Python uses the return value of self._trace_func as the local trace for the called function. If we only have _trace_func return itself, tracing wouldn't actually be activated for the called functions.
There was a problem hiding this comment.
Okay yes, I recall you also explained this in your presentation last week.
In this case it seems to me like we should leave _trace_func unchanged and instead define a separate global tracing function that only handles function calls. This will help keep the code more modular and make clear the two separate purposes.
| func_frame.f_trace = self._trace_func | ||
| sys.settrace(lambda *_args: None) | ||
| self._origin_file = os.path.normcase( | ||
| os.path.abspath(func_frame.f_globals.get("__file__", "")) |
There was a problem hiding this comment.
"" is not a good default. If the file cannot be found, let's be conservative and do not trace into any function calls.
| self._traced_frames.append( | ||
| { | ||
| "co_firstlineno": frame.f_code.co_firstlineno, | ||
| "source_lines": inspect.getsource(frame.f_code).splitlines(), |
There was a problem hiding this comment.
Overall this approach is storing more than it needs.
The existing _first_line attribute can be reused but renamed to _start_lineno; its role should be changed to store the minimum line number that should be displayed. Similarly, you can add an _end_lineno attribute to store the maximum line number that should be displayed. Both can be updated directly here, instead of introducing a new self._trace_frames attribute.
In _get_code below, right now _first_line is being used to index into just the source lines of the original function frame. We can greatly simplify by (1) getting the full lines of the module rather than just the single function, and then (2) directly indexing into that list using _start_lineno and _end_lineno, without doing any offsetting.
| <script> | ||
| window.codeText = `{{ code_text }}`; | ||
| window.memoryVizData = {{ memory_viz_data | tojson }}; | ||
| window.startLineNumber = {{ start_line_number }}; |
There was a problem hiding this comment.
Do not make changes to the webstepper in this PR. This PR should deal entirely with the logical changes necessary to do the tracing into function calls. This is why I said:
To avoid the issue you ran into in the earlier webstepper PR, you could try testing this with only functions that are defined below the
SnapshotTracercontext block.
| """Take a snapshot of the variables in the functions specified in `self.include`, tracing into same-module function calls.""" | ||
| if event == "call": | ||
| # Only trace functions in the same module as the calling function. | ||
| called_file = os.path.normcase(os.path.abspath(frame.f_globals.get("__file__", ""))) |
There was a problem hiding this comment.
Similar to my comment below, if no __file__ attribute is found, do not trace into the call.
|
|
||
| 1. Due to differences in Python interpreters, this context manager only works with Python versions >= 3.10. | ||
| 2. The context manager does not step into any function calls. Calling functions within the traced function may lead to undefined behavior. | ||
| 2. The context manager only steps into Python functions defined in the same module as the traced context. Calls into external modules and builtins are not traced. |
There was a problem hiding this comment.
This wording change is good, but I think we should move this out of the "limitations" section and instead have this be a general description of the function tracing behaviour for SnapshotTracer.
| self._origin_file = os.path.normcase( | ||
| os.path.abspath(func_frame.f_globals.get("__file__", "")) | ||
| ) | ||
| sys.settrace(self._trace_func) |
There was a problem hiding this comment.
Okay yes, I recall you also explained this in your presentation last week.
In this case it seems to me like we should leave _trace_func unchanged and instead define a separate global tracing function that only handles function calls. This will help keep the code more modular and make clear the two separate purposes.
| self._origin_file = None | ||
| self._module_source_lines = None | ||
|
|
||
| def _global_trace_func(self, frame: types.FrameType, event: str, _arg: Any) -> Any: |
There was a problem hiding this comment.
Oh I think I missed this last time, but the return type here can be simplified (it's either a Callable or None)
|
|
||
| def _global_trace_func(self, frame: types.FrameType, event: str, _arg: Any) -> Any: | ||
| """Global trace function that handles 'call' events to determine which functions to trace into.""" | ||
| if event == "call": |
There was a problem hiding this comment.
By splitting this off into a separate helper you can simplify the logic a bit. I would have one guard at the very start of the function for an early return (if event != "call" or self._origin_file is None).
| # Only trace functions in the same module as the calling function. | ||
| called_file = os.path.normcase(os.path.abspath(frame.f_code.co_filename)) | ||
| if called_file == self._origin_file and frame.f_code.co_name not in ( | ||
| "_trace_func", |
There was a problem hiding this comment.
Oh I missed this too, but please use self._trace_func.__name__, and similarly for the other one. This is better than string constants, as it'll be detected as a usage of the methods, which is good if we ever rename them.
david-yz-liu
left a comment
There was a problem hiding this comment.
Nice work, @rachelzUT!
Proposed Changes
Modified
SnapshotTracerto trace into calls of functions defined in the same module thatSnapshotTraceris called, asSnapshotTracerpreviously only traced lines of code within the context block and did not trace into any function calls within the block. Note that built-in functions and functions from external libraries/modules are not traced into....
Screenshots of your changes (if applicable)
Type of Change
Checklist
Before opening your pull request:
After opening your pull request:
Questions and Comments
packages/python-ta/docs/debug/index.mdwith the new feature. Before it was listed under "Current Limitations" which I just edited to reflect the change, but if it's no longer considered a 'limitation' since this fixes that limitation, I can move the description elsewhere.startLineNumberwhich will ensure the correct line numbers and highlighting are shown in the code section of the Webstepper