Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sys.monitoring: local events still sent after free_tool_id #111963

Closed
nedbat opened this issue Nov 10, 2023 · 17 comments
Closed

sys.monitoring: local events still sent after free_tool_id #111963

nedbat opened this issue Nov 10, 2023 · 17 comments
Assignees
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@nedbat
Copy link
Member

nedbat commented Nov 10, 2023

Bug report

Bug description:

I use set_local_events with a tool id. Later I free the tool id, but then still get those events. Shouldn't freeing the tool id stop all events?

import sys
print(sys.version)

sysmon = sys.monitoring
events = sysmon.events
myid = 1

def simple_function():
    print("Inside simple function")

def sysmon_py_start(code, instruction_offset):
    print(f"sysmon_py_start({code=}, {instruction_offset=})")
    sysmon.set_local_events(myid, code, events.LINE)

def sysmon_line(code, line_number):
    print(f"sysmon_line({code=}, {line_number=})")

print("Registering")
sysmon.use_tool_id(myid, "try669.py")
sysmon.set_events(myid, events.PY_START)

sysmon.register_callback(myid, events.PY_START, sysmon_py_start)
sysmon.register_callback(myid, events.LINE, sysmon_line)

simple_function()

print("Freeing")
sysmon.set_events(myid, 0)
sysmon.free_tool_id(myid)

simple_function()

When I run this, I see:

3.12.0 (main, Oct  2 2023, 13:16:02) [Clang 14.0.3 (clang-1403.0.22.14.1)]
Registering
sysmon_py_start(code=<code object simple_function at 0x10ead1530, file "/private/tmp/try669.py", line 8>, instruction_offset=0)
sysmon_line(code=<code object simple_function at 0x10ead1530, file "/private/tmp/try669.py", line 8>, line_number=9)
Inside simple function
Freeing
sysmon_line(code=<code object simple_function at 0x10ead1530, file "/private/tmp/try669.py", line 8>, line_number=9)
Inside simple function

Shouldn't freeing the tool id stop sysmon_line from being called again?

The same behavior happens with 3.13.0a1.

cc @markshannon

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

macOS

Linked PRs

@nedbat nedbat added the type-bug An unexpected behavior, bug, or error label Nov 10, 2023
@AlexWaygood AlexWaygood added 3.12 bugs and security fixes 3.13 bugs and security fixes labels Nov 10, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 10, 2023
@brandtbucher
Copy link
Member

Agreed, events should be unset and callbacks should be unregistered when free_tool_id is called.

@gaogaotiantian
Copy link
Member

For now I don't think whether the tool_id is used is checked at all in the instrumentation process. It was only checked on higher level functions like get_events().

It's NOT against the documentation, but I also think it might be better to free all the hooks once the tool_id is freed.

Actually, I'd propose another API sys.monitoring.clear_tool_id() to just clear all the hooks while keeping the tool_id.

However, I believe the reason this is the way it is now, is because instrumentation is lazy-loaded. We don't track all the instrumented code objects and de-instrument them immediately once an event is unset.

Consider the following scenario:

def f():
    pass
sys.monitoring.use_tool_id(1, "tool1")
sys.set_local_events(1, f.__code__, LINE)
sys.monitoring.free_tool_id(1, "tool1")
sys.monitoring.use_tool_id(1, "tool2")
# What would happen here? Does the event fire?

As the instrumentation won't even trigger before f() executes, when f() executes, it'll think tool_id is used - but it's actually already freed once.

In order to achieve that, we may need to put a version number on all events, and on tool_id. That solution might not need a potentially growing piece of memory if the user just toggles tool_id for fun.

Just saying - it's not a trivial issue which we can resolve by adding an if statement somewhere.

@brandtbucher
Copy link
Member

brandtbucher commented Nov 10, 2023

Interesting. So it seems fairly straightforward to unregister callbacks, and clear global events, but not to clear local events.

In your example, though, I'd definitely expect neither tool to see the line events on f. I guess I'd be +1 on versioning the set of local events per code object to make that happen.

@gaogaotiantian
Copy link
Member

Actually, global events are lazily instrumented as well. So no that won't work either. Unregister callbacks are easy but that would be even worse if the events are not disabled.

@gaogaotiantian
Copy link
Member

For the version, it would be one version per event (local + global) per code object, plus a version for the tool id. Basically we need to version everything.

@brandtbucher
Copy link
Member

Hm, okay. This is definitely harder than it seems.

@gaogaotiantian
Copy link
Member

Yeah I guess that's why Mark did not do it to begin with. None of the instrumentation commands take effects immediately so the instrumentation need to somehow depend on the current state only.

We can disable the events if the tool_id is not currently used, that's easy, but that just hides the dirt under the carpet. Or we could say that it's the user's responsibility to clean everything up before freeing the id - it's a low level API.

If we do want a clean mechanism, some level of restructure might be needed unless I'm missing something obvious.

nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 11, 2023
@nedbat
Copy link
Member Author

nedbat commented Nov 11, 2023

I've adapted by holding a WeakSet of the code objects I've set events on, and using it to clear the events when I'm shutting down. This seems to work. At least the documentation could be clearer on this.

@nedbat
Copy link
Member Author

nedbat commented Nov 11, 2023

But further experimentation drew me into the question of equality of code objects. It looks like the code_richcompare function looks at the bytecodes themselves. Doesn't the specializing interpreter change the bytecodes? Apart from the WeakSet of code objects, I'm using a dict with code object keys, and seeing some mysterious behavior where code objects are added and then can't be found. Could this be a problem?

@brandtbucher
Copy link
Member

Code object hashes shouldn’t change. Last I checked, when comparing the bytecode for hashing and equality purposes, we deoptimize each instruction so that the hashing and equality look the same as for a “fresh” code object. Ditto for instrumented instructions.

It’s a bit finicky though, so I wouldn’t be totally surprised if there’s a case where we compute the wrong result in the presence of instrumentation or specialization. Do you have a reproducer?

@nedbat
Copy link
Member Author

nedbat commented Nov 11, 2023

The code object hash/equality problem is its own issue: #111984

@nedbat
Copy link
Member Author

nedbat commented Nov 11, 2023

There's another problem with keeping a set of code objects: two different code objects can hash and compare equal, so only one of them will be kept. That would mean there would be code objects that didn't get unregistered. What's the right way to do this?

@nedbat
Copy link
Member Author

nedbat commented Nov 12, 2023

Never mind, I'm using the id of the code objects as the keys. Two different code objects being equal means code objects as keys won't work for me.

nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 12, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 18, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 18, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 18, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 19, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 20, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 20, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 20, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 20, 2023
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Nov 21, 2023
…ythonGH-112291)

(cherry picked from commit 46500c4)

Co-authored-by: Tian Gao <gaogaotiantian@hotmail.com>
@brandtbucher
Copy link
Member

Docs have been updated to clarify current behavior. Should I leave this open to continue exploring changes to that behavior?

brandtbucher pushed a commit that referenced this issue Nov 21, 2023
…H-112304)

(cherry picked from commit 46500c4)
Co-authored-by: Tian Gao <gaogaotiantian@hotmail.com>
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 22, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 22, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 25, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 26, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 26, 2023
@iritkatriel iritkatriel added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Nov 26, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 29, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 29, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 29, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 30, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Nov 30, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 2, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 4, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 4, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 6, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 11, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 12, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 14, 2023
nedbat added a commit to nedbat/coveragepy that referenced this issue Dec 24, 2023
@nedbat
Copy link
Member Author

nedbat commented Jul 30, 2024

I think this issue can be closed.

@gaogaotiantian
Copy link
Member

Hi @nedbat. We added a clear_tool_id API in #124568 to clear all the callbacks and events (global and local) for a tool_id. Calling free_tool_id will also call that function first so all the registered instrumentation will be reverted. Hope that makes you happy :)

I'll close this issue now because it's really fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

6 participants