classification
Title: Allow helper functions to wrap sys.setprofile
Type: enhancement Stage:
Components: Library (Lib) Versions: Python 3.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: benjamin.peterson, christian.heimes, giampaolo.rodola, louielu, ncoghlan
Priority: normal Keywords:

Created on 2017-04-20 09:36 by louielu, last changed 2017-05-13 05:24 by louielu.

Pull Requests
URL Status Linked Edit
PR 1212 open louielu, 2017-04-20 10:22
PR 1253 open louielu, 2017-04-22 09:41
Messages (4)
msg291957 - (view) Author: Louie Lu (louielu) * Date: 2017-04-20 09:36
This is a sub-problem of #9285, in #9285, we aim to provide cProfile and profile a context manager, this will need to add code like this:

    def __enter__(self):
        self.set_cmd('')
        sys.setprofile(self.dispatcher)
        return self

Unfortunately, when setting up profiler via `sys.setprofile`, it will immediately work on next line `return self`, which cause the assertion inside `trace_dispatch_return` claim this is a "Bad return".

Technically, `profile.Profile` can not go return upper than it's frame inside the frame stack. This behavior can be observed by this code:

    def fib(n):
        if n > 2:
            return fib(n - 1) + fib(n - 2)
        return n

    def foo():
        pr = profile.Profile()
        # Profile was set in the `foo` frame, it can't get more upper than this
        # that means, we can't return to global frame when this profile is set
        sys.setprofile(pr.dispatcher)
        fib(5)
        # We didn't stop the profile here via sys.setprofile(None)
        # So it will return 0xDEADBEAF to global frame and cause a bad return
        return 0xDEADBEAF

    foo()

Here this issue will provide the test of this behavior, then will make some modify in #9285 to prevent this situation when using profile as a context manager.
msg292107 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2017-04-22 05:55
Thanks Louie.

I've adjusted the issue title, as I think the core RFE is that we'd like the following helper function to work exactly the same way as calling sys.setprofile directly:

    def setprofile_helper(f):
        sys.setprofile(f)

The following utility function can be used to demonstrate the current discrepancy:

    def profile_call(setprofile, call):
        pr = profile.Profile()
        setprofile(pr.dispatcher)
        try:
            call()
        finally:
            setprofile(None)
        return pr

Specifically:

```
>>> profile_call(sys.setprofile, lambda:print("Profiled!"))
Profiled!
<profile.Profile object at 0x7fb2265966d8>
>>> profile_call(setprofile_helper, lambda:print("Profiled!"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in profile_call
  File "<stdin>", line 2, in setprofile_helper
  File "/usr/lib64/python3.5/profile.py", line 209, in trace_dispatch_i
    if self.dispatch[event](self, frame, t):
  File "/usr/lib64/python3.5/profile.py", line 293, in trace_dispatch_return
    assert frame is self.cur[-2].f_back, ("Bad return", self.cur[-3])
AssertionError: ('Bad return', ('profile', 0, 'profiler'))
```

If we can figure out a way to do it reliably, the most desirable approach would be to make helper functions like the above "just work", perhaps by remembering the stack frame where the profiler was *defined*, and using that as the reference point to construct the synthetic stack the first time a trace dispatch occurs.

If that transparent-to-the-user approach doesn't appear to be feasible, an alternative would be to offer a new private method on Profile objects to adjust their synthetic stack by one level, as if `sys.setprofile` had been called *before* the call into the helper function.

That is, a profiler-aware alternative to the helper function above might look like:

    def profile_helper(pr):
        sys.setprofile(pr.dispatcher)
        pr._adjust_stack()

and that's then the approach that the `__enter__` method would use to implement context management support.

The advantage of the latter private option is that it wouldn't need to support arbitrary helper functions, just the specific implementation of `__enter__` for issue 9285.

In either case, I *don't* think changing the assertion is the right answer - instead, I think the internal profiler state needs to be adjusted so the assertion passes without modification.
msg292112 - (view) Author: Louie Lu (louielu) * Date: 2017-04-22 09:20
Thanks, Nick. Your analysis is very helpful.


After some testing, I found the problem here is because when we using `sys.setprofile` in the helper function, we didn't simulate the call (from where profiler create to helper function), that cause profile's frame link breakup, so if we want to return to outside of helper function, it will report a bad return.

A straightforward method is the latter method you propose, to make a profiler-aware function manually insert the frame into profiler's frame stack:

    def _adjust_frame(self):
        frame = sys._getframe(1)  # Get helper function frame
        self.dispatch['call'](self, frame, 0)

And adjust *before* install profiler:

    def profile_helper(pr):
        pr._adjust_frame()
        sys.setprofile(pr.dispatcher)


Then we can get the correct thing we need.
msg293593 - (view) Author: Louie Lu (louielu) * Date: 2017-05-13 05:24
PR-1212 Add an explicit test for dispatch_return's assertion, could @benjaminp, @tiran or @giampaolo help to review this? Thanks!
History
Date User Action Args
2017-05-13 05:24:44louielusetnosy: + giampaolo.rodola, christian.heimes, benjamin.peterson
messages: + msg293593
2017-04-22 09:41:37louielusetpull_requests: + pull_request1368
2017-04-22 09:20:28louielusetmessages: + msg292112
2017-04-22 05:55:20ncoghlansetmessages: + msg292107
title: Add profile test case for trace_dispatch_return assertion -> Allow helper functions to wrap sys.setprofile
2017-04-20 10:22:07louielusetpull_requests: + pull_request1334
2017-04-20 09:36:32louielucreate