From 0fe55c6d125518243667edd6a5572ff20380c3f4 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Thu, 4 Aug 2022 15:04:26 +0100 Subject: [PATCH] PEP 669: Add multiple tool and one-shot event handling capabilities (#2747) --- pep-0669.rst | 252 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 189 insertions(+), 63 deletions(-) diff --git a/pep-0669.rst b/pep-0669.rst index c81e5fc4b..e337000cc 100644 --- a/pep-0669.rst +++ b/pep-0669.rst @@ -14,12 +14,12 @@ Abstract Using a profiler or debugger in CPython can have a severe impact on performance. Slowdowns by an order of magnitude are common. -This PEP proposes an API for monitoring of Python programs running +This PEP proposes an API for monitoring Python programs running on CPython that will enable monitoring at low cost. Although this PEP does not specify an implementation, it is expected that it will be implemented using the quickening step of -:pep:`PEP 659 <659#quickening>`. +:pep:`659`. A ``sys.monitoring`` namespace will be added, which will contain the relevant functions and enum. @@ -44,9 +44,9 @@ the parts of the code that are modified and a relatively low cost to those parts that are modified. We can leverage this to provide an efficient mechanism for monitoring that was not possible in 3.10 or earlier. -By using quickening, we expect that code run under a debugger on 3.11 -should easily outperform code run without a debugger on 3.10. -Profiling will still slow down execution, but by much less than in 3.10. +By using quickening, we expect that code run under a debugger on 3.12 +should outperform code run without a debugger on 3.11. +Profiling will still slow down execution, but by much less than in 3.11. Specification @@ -57,6 +57,9 @@ for events and by activating a set of events. Activating events and registering callback functions are independent of each other. +Both registering callbacks and activating events are done on a per-tool basis. +It is possible to have multiple tools that respond to different sets of events. + Events ------ @@ -65,16 +68,16 @@ to tools. By activating events and by registering callback functions tools can respond to these events in any way that suits them. Events can be set globally, or for individual code objects. -For 3.11, CPython will support the following events: +For 3.12, CPython will support the following events: -* PY_CALL: Call of a Python function (occurs immediately after the call, the callee's frame will be on the stack) +* PY_START: Start of a Python function (occurs immediately after the call, the callee's frame will be on the stack) * PY_RESUME: Resumption of a Python function (for generator and coroutine functions), except for throw() calls. * PY_THROW: A Python function is resumed by a throw() call. * PY_RETURN: Return from a Python function (occurs immediately before the return, the callee's frame will be on the stack). * PY_YIELD: Yield from a Python function (occurs immediately before the yield, the callee's frame will be on the stack). * PY_UNWIND: Exit from a Python function during exception unwinding. -* C_CALL: Call of a builtin function (before the call in this case). -* C_RETURN: Return from a builtin function (after the return in this case). +* C_CALL: Call to any callable, except Python functions (before the call in this case). +* C_RETURN: Return from any callable, except Python functions (after the return in this case). * RAISE: An exception is raised. * EXCEPTION_HANDLED: An exception is handled. * LINE: An instruction is about to be executed that has a different line number from the preceding instruction. @@ -93,15 +96,48 @@ All events will be attributes of the ``Event`` enum in ``sys.monitoring``:: Note that ``Event`` is an ``IntFlag`` which means that the events can be or-ed together to form a set of events. +Tool identifiers +---------------- + +The VM can support up to 6 tools at once. +Before registering or activating events, a tool should choose an identifier. +Identifiers are integers in the range 0 to 5. + +:: + + sys.monitoring.use_tool_id(id)->None + sys.monitoring.free_tool_id(id)->None + +``sys.monitoring.use_tool_id`` raises a ``ValueError`` if ``id`` is in use. + +All IDs are treated the same by the VM with regard to events, but the following +IDs are pre-defined to make co-operation of tools easier:: + + sys.monitoring.DEBUGGER_ID = 0 + sys.monitoring.COVERAGE_ID = 1 + sys.monitoring.PROFILER_ID = 2 + sys.monitoring.OPTIMIZER_ID = 3 + +There is no obligation to set an ID, nor is there anything preventing a tool from +using an ID even it is already in use. +However, tool are encouraged to use a unique ID and respect other tools. + +For example, if a debugger were attached and ``DEBUGGER_ID`` were in use, it should +report an error, rather than carrying on regardless. + +The ``OPTIMIZER_ID`` is provided for tools like Cinder or PyTorch +that want to optimize Python code, but need to decide what to +optimize in a way that depends on some wider context. + Setting events globally ----------------------- Events can be controlled globally by modifying the set of events being monitored: -* ``sys.monitoring.get_events()->Event`` +* ``sys.monitoring.get_events(tool_id:int)->Event`` Returns the ``Event`` set for all the active events. -* ``sys.monitoring.set_events(event_set: Event)`` +* ``sys.monitoring.set_events(tool_id:int, event_set: Event)`` Activates all events which are set in ``event_set``. No events are active by default. @@ -111,10 +147,10 @@ Per code object events Events can also be controlled on a per code object basis: -* ``sys.monitoring.get_local_events(code: CodeType)->Event`` +* ``sys.monitoring.get_local_events(tool_id:int, code: CodeType)->Event`` Returns the ``Event`` set for all the local events for ``code`` -* ``sys.monitoring.set_local_events(code: CodeType, event_set: Event)`` +* ``sys.monitoring.set_local_events(tool_id:int, code: CodeType, event_set: Event)`` Activates all the local events for ``code`` which are set in ``event_set``. Local events add to global events, but do not mask them. @@ -126,12 +162,12 @@ Register callback functions To register a callable for events call:: - sys.monitoring.register_callback(event, func) + sys.monitoring.register_callback(tool_id:int, event: Event, func: Callable | None) ``register_callback`` returns the previously registered callback, or ``None``. Functions can be unregistered by calling -``sys.monitoring.register_callback(event, None)``. +``sys.monitoring.register_callback(tool_id, event, None)``. Callback functions can be registered and unregistered at any time. @@ -145,23 +181,23 @@ Different events will provide the callback function with different arguments, as * All events starting with ``PY_``: - ``func(code: CodeType, instruction_offset: int)`` + ``func(code: CodeType, instruction_offset: int) -> DISABLE | Any`` * ``C_CALL`` and ``C_RETURN``: - ``func(code: CodeType, instruction_offset: int, callable: object)`` + ``func(code: CodeType, instruction_offset: int, callable: object) -> DISABLE | Any`` * ``RAISE`` and ``EXCEPTION_HANDLED``: - ``func(code: CodeType, instruction_offset: int, exception: BaseException)`` + ``func(code: CodeType, instruction_offset: int, exception: BaseException) -> DISABLE | Any`` * ``LINE``: - ``func(code: CodeType, line_number: int)`` + ``func(code: CodeType, line_number: int) -> DISABLE | Any`` -* ``JUMP`` and ``BRANCH``: +* ``BRANCH``: - ``func(code: CodeType, instruction_offset: int, destination_offset: int)`` + ``func(code: CodeType, instruction_offset: int, destination_offset: int) -> DISABLE | Any`` Note that the ``destination_offset`` is where the code will next execute. For an untaken branch this will be the offset of the instruction following @@ -169,86 +205,169 @@ Different events will provide the callback function with different arguments, as * ``INSTRUCTION``: - ``func(code: CodeType, instruction_offset: int)`` + ``func(code: CodeType, instruction_offset: int) -> DISABLE | Any`` * ``MARKER``: - ``func(code: CodeType, instruction_offset: int, marker_id: int)`` + ``func(code: CodeType, instruction_offset: int) -> DISABLE | Any`` + +If a callback returns ``sys.monitoring.DISABLE`` then that tool will not +recieve any more events for that ``(code, instruction_offset)``. + +This feature is provided for coverage and other tools that are only interested +seeing an event once. + +Tools may see events after returning ``DISABLE``, in which case, they will not see +those events until ``sys.monitoring.restart_events()`` is called. +Note that ``sys.monitoring.restart_events()`` is not specific to one tool, +so tools must be prepared to recieve events that they have chosen to DISABLE. + +Events in callback functions +---------------------------- + +Events are suspended in callback functions and their callees for the tool +that registered that callback. + +That means that other tools will see events in the callback functions for other +tools. This could be useful for debugging a profiling tool, but would produce +misleading profiles, as the debugger tool would show up in the profile. Inserting and removing markers -'''''''''''''''''''''''''''''''''' +------------------------------ Two new functions are added to the ``sys`` module to support markers. -* ``sys.monitoring.insert_marker(code: CodeType, offset: int, marker_id=0: range(256))`` -* ``sys.monitoring.remove_marker(code: CodeType, offset: int)`` +* ``sys.monitoring.insert_marker(tool_id: int, code: CodeType, offset: int)`` +* ``sys.monitoring.remove_marker(tool_id: int, code: CodeType, offset: int)`` -The ``marker_id`` has no meaning to the VM, -and is used only as an argument to the callback function. -The ``marker_id`` must in the range 0 to 255 (inclusive). +A single code object may not have more than 255 markers at once. +``sys.monitoring.insert_marker`` raises a ``ValueError`` if this limit +is exceeded. + +Order of events +--------------- + +If an instructions triggers several events the occur in the following order: + +* MARKER +* INSTRUCTION +* LINE +* All other events (only one of these event can occur per instruction) + +Each event is delivered to tools in ascending order of ID. Attributes of the ``sys.monitoring`` namespace -'''''''''''''''''''''''''''''''''''''''''''''' +---------------------------------------------- * ``class Event(enum.IntFlag)`` -* ``def get_events()->Event`` -* ``def set_events(event_set: Event)->None`` -* ``def get_local_events(code: CodeType)->Event`` -* ``def set_local_events(code: CodeType, event_set: Event)->None`` -* ``def register_callback(event: Event, func: Callable)->Optional[Callable]`` -* ``def insert_marker(code: CodeType, offset: Event, marker_id=0: range(256))->None`` -* ``def remove_marker(code: CodeType, offset: Event)->None`` +* ``def use_tool_id(id)->None`` +* ``def free_tool_id(id)->None`` +* ``def get_events(tool_id: int)->Event`` +* ``def set_events(tool_id: int, event_set: Event)->None`` +* ``def get_local_events(tool_id: int, code: CodeType)->Event`` +* ``def set_local_events(tool_id: int, code: CodeType, event_set: Event)->None`` +* ``def register_callback(tool_id: int, event: Event, func: Callable)->Optional[Callable]`` +* ``def insert_marker(tool_id: int, code: CodeType, offset: Event)->None`` +* ``def remove_marker(tool_id: int, code: CodeType, offset: Event)->None`` +* ``def restart_events()->None`` +* ``DISABLE: object`` + Backwards Compatibility ======================= -This PEP is fully backwards compatible, in the sense that old code -will work if the features of this PEP are unused. +This PEP is mostly backwards compatible. -However, if it is used it will effectively disable ``sys.settrace``, -``sys.setprofile`` and :pep:`523` frame evaluation. +This PEP is incompatible with :pep:`523` as the behavior would be undefined, +as we have no control over the behavior of :pep:`523` plugins. -If :pep:`523` is in use, or ``sys.settrace`` or ``sys.setprofile`` has been -set, then calling ``sys.monitoring.set_events()`` or +Thus, if :pep:`523` is in use, then calling ``sys.monitoring.set_events()`` or ``sys.monitoring.set_local_events()`` will raise an exception. Likewise, if ``sys.monitoring.set_events()`` or ``sys.monitoring.set_local_events()`` has been called, then using :pep:`523` -or calling ``sys.settrace`` or ``sys.setprofile`` will raise an exception. +will raise an exception. -This PEP is incompatible with ``sys.settrace`` and ``sys.setprofile`` -because the implementation of ``sys.settrace`` and ``sys.setprofile`` -will use the same underlying mechanism as this PEP. It would be too slow -to support both the new and old monitoring mechanisms at the same time, -and they would interfere in awkward ways if both were active at the same time. +``sys.settrace`` and ``sys.setprofile`` will act as if they were tools 6 and 7 +respectively, so can be used along side this PEP. -This PEP is incompatible with :pep:`523`, because :pep:`523` prevents the VM being -able to modify the code objects of executing code, which is a necessary feature. - -We may seek to remove ``sys.settrace`` and :pep:`523` in the future once the APIs -provided by this PEP have been widely adopted, but that is for another PEP. +This makes ``sys.settrace`` and ``sys.setprofile`` incompatible with :pep:`523`. +Arguably, they already were as the author know of any PEP 523 plugin that support +``sys.settrace`` or ``sys.setprofile`` correctly. This PEP merely formalizes that. Performance ----------- -If no events are active, this PEP should have a negligible impact on -performance. +If no events are active, this PEP should have a small positive impact on +performance. Experiments show between 1 and 2% speedup from not supporting +``sys.settrace()`` directly. + +The performance of ``sys.settrace()`` will be worse. +The performance of ``sys.setprofile()`` should be better. +However, by the tools relying on ``sys.settrace()`` and ``sys.setprofile()`` +can be made a lot faster by using the API provided by this PEP. If a small set of events are active, e.g. for a debugger, then the overhead of callbacks will be orders of magnitudes less than for ``sys.settrace`` and much cheaper than using :pep:`523`. +Coverage tools can be implemented at very low cost, +by returning ``DISABLE`` in all callbacks. + For heavily instrumented code, e.g. using ``LINE``, performance should be better than ``sys.settrace``, but not by that much as performance will be dominated by the time spent in callbacks. For optimizing virtual machines, such as future versions of CPython -(and ``PyPy`` should they choose to support this API), changing the set of -globally active events in the midst of a long running program could be quite +(and ``PyPy`` should they choose to support this API), changes to the set +active events in the midst of a long running program could be quite expensive, possibly taking hundreds of milliseconds as it triggers de-optimizations. Once such de-optimization has occurred, performance should recover as the VM can re-optimize the instrumented code. +In general these operations can be considered to be fast: + +* ``def get_events(tool_id: int)->Event`` +* ``def get_local_events(tool_id: int, code: CodeType)->Event`` +* ``def register_callback(tool_id: int, event: Event, func: Callable)->Optional[Callable]`` + +These operations are slower, but not especially so: + +* ``def set_local_events(tool_id: int, code: CodeType, event_set: Event)->None`` +* ``def insert_marker(tool_id: int, code: CodeType, offset: Event)->None`` +* ``def remove_marker(tool_id: int, code: CodeType, offset: Event)->None`` + +And these operations should be regarded as slow: + +* ``def use_tool_id(id)->None`` +* ``def free_tool_id(id)->None`` +* ``def set_events(tool_id: int, event_set: Event)->None`` +* ``def restart_events()->None`` + +How slow, the slow operation the operations are, depends on when then happen. +If done early in the program, before modules are loaded, +they should be fairly inexpensive. + +Memory Consumption +'''''''''''''''''' + +When not in use, this PEP will have a neglible change on memory consumption. + +How memory is used is very much an implementation detail. +However, we expect that for 3.12 the additional memory consumption per +code object will be **roughly** as follows: + ++-------------+--------+--------+-------------+ +| | Events | ++-------------+--------+--------+-------------+ +| Tools | Others | LINE | INSTRUCTION | ++=============+========+========+=============+ +| One | None | ≈40% | ≈80% | ++-------------+--------+--------+-------------+ ++ Two or more | ≈40% | ≈120% | ≈200% | ++-------------+--------+--------+-------------+ + + Security Implications ===================== @@ -269,8 +388,9 @@ step of :pep:`PEP 659 <659#quickening>`. Activating some events will cause all code objects to be quickened before they are executed. -For example, if the ``LINE`` event is turned on, then all instructions that -are at the start of a line will be replaced with a ``LINE_EVENT`` instruction. +For example, if the ``C_CALL`` event is turned on, +then all call instructions will be +replaced with a ``INSTRUMENTED_CALL`` instruction. Note that this will interfere with specialization, which will result in some performance degradation in addition to the overhead of calling the @@ -289,7 +409,7 @@ underlying event occurs. The exact set of events that require instrumentation is an implementation detail, but for the current design, the following events will require instrumentation: -* PY_CALL +* PY_START * PY_RESUME * PY_RETURN * PY_YIELD @@ -300,6 +420,13 @@ but for the current design, the following events will require instrumentation: * JUMP * BRANCH +Each instrumented bytecode will require an additional 8 bits of information to +note which tool the instrumentation applies to. +``LINE`` and ``INSTRUCTION`` events require additional information, as they +need to store the original instruction, or even the instrumented instruction +if they overlap other instrumentation. + + Implementing tools ================== @@ -351,7 +478,6 @@ Debuggers can use the ``PY_CALL``, etc. events to be informed when a code object is first encountered, so that any necessary breakpoints can be inserted. - Coverage Tools -------------- @@ -383,7 +509,7 @@ Line based profilers Line based profilers can use the ``LINE`` and ``JUMP`` events. Implementers of profilers should be aware that instrumenting ``LINE`` -and ``JUMP`` events will have a large impact on performance. +events will have a large impact on performance. .. note::