486 lines
12 KiB
Python
486 lines
12 KiB
Python
"""
|
|
The ``numba.core.event`` module provides a simple event system for applications
|
|
to register callbacks to listen to specific compiler events.
|
|
|
|
The following events are built in:
|
|
|
|
- ``"numba:compile"`` is broadcast when a dispatcher is compiling. Events of
|
|
this kind have ``data`` defined to be a ``dict`` with the following
|
|
key-values:
|
|
|
|
- ``"dispatcher"``: the dispatcher object that is compiling.
|
|
- ``"args"``: the argument types.
|
|
- ``"return_type"``: the return type.
|
|
|
|
- ``"numba:compiler_lock"`` is broadcast when the internal compiler-lock is
|
|
acquired. This is mostly used internally to measure time spent with the lock
|
|
acquired.
|
|
|
|
- ``"numba:llvm_lock"`` is broadcast when the internal LLVM-lock is acquired.
|
|
This is used internally to measure time spent with the lock acquired.
|
|
|
|
- ``"numba:run_pass"`` is broadcast when a compiler pass is running.
|
|
|
|
- ``"name"``: pass name.
|
|
- ``"qualname"``: qualified name of the function being compiled.
|
|
- ``"module"``: module name of the function being compiled.
|
|
- ``"flags"``: compilation flags.
|
|
- ``"args"``: argument types.
|
|
- ``"return_type"`` return type.
|
|
|
|
Applications can register callbacks that are listening for specific events using
|
|
``register(kind: str, listener: Listener)``, where ``listener`` is an instance
|
|
of ``Listener`` that defines custom actions on occurrence of the specific event.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import atexit
|
|
import abc
|
|
import enum
|
|
import time
|
|
import threading
|
|
from timeit import default_timer as timer
|
|
from contextlib import contextmanager, ExitStack
|
|
from collections import defaultdict
|
|
|
|
from numba.core import config
|
|
|
|
|
|
class EventStatus(enum.Enum):
|
|
"""Status of an event.
|
|
"""
|
|
START = enum.auto()
|
|
END = enum.auto()
|
|
|
|
|
|
# Builtin event kinds.
|
|
_builtin_kinds = frozenset([
|
|
"numba:compiler_lock",
|
|
"numba:compile",
|
|
"numba:llvm_lock",
|
|
"numba:run_pass",
|
|
])
|
|
|
|
|
|
def _guard_kind(kind):
|
|
"""Guard to ensure that an event kind is valid.
|
|
|
|
All event kinds with a "numba:" prefix must be defined in the pre-defined
|
|
``numba.core.event._builtin_kinds``.
|
|
Custom event kinds are allowed by not using the above prefix.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
|
|
Return
|
|
------
|
|
res : str
|
|
"""
|
|
if kind.startswith("numba:") and kind not in _builtin_kinds:
|
|
msg = (f"{kind} is not a valid event kind, "
|
|
"it starts with the reserved prefix 'numba:'")
|
|
raise ValueError(msg)
|
|
return kind
|
|
|
|
|
|
class Event:
|
|
"""An event.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
status : EventStatus
|
|
data : any; optional
|
|
Additional data for the event.
|
|
exc_details : 3-tuple; optional
|
|
Same 3-tuple for ``__exit__``.
|
|
"""
|
|
def __init__(self, kind, status, data=None, exc_details=None):
|
|
self._kind = _guard_kind(kind)
|
|
self._status = status
|
|
self._data = data
|
|
self._exc_details = (None
|
|
if exc_details is None or exc_details[0] is None
|
|
else exc_details)
|
|
|
|
@property
|
|
def kind(self):
|
|
"""Event kind
|
|
|
|
Returns
|
|
-------
|
|
res : str
|
|
"""
|
|
return self._kind
|
|
|
|
@property
|
|
def status(self):
|
|
"""Event status
|
|
|
|
Returns
|
|
-------
|
|
res : EventStatus
|
|
"""
|
|
return self._status
|
|
|
|
@property
|
|
def data(self):
|
|
"""Event data
|
|
|
|
Returns
|
|
-------
|
|
res : object
|
|
"""
|
|
return self._data
|
|
|
|
@property
|
|
def is_start(self):
|
|
"""Is it a *START* event?
|
|
|
|
Returns
|
|
-------
|
|
res : bool
|
|
"""
|
|
return self._status == EventStatus.START
|
|
|
|
@property
|
|
def is_end(self):
|
|
"""Is it an *END* event?
|
|
|
|
Returns
|
|
-------
|
|
res : bool
|
|
"""
|
|
return self._status == EventStatus.END
|
|
|
|
@property
|
|
def is_failed(self):
|
|
"""Is the event carrying an exception?
|
|
|
|
This is used for *END* event. This method will never return ``True``
|
|
in a *START* event.
|
|
|
|
Returns
|
|
-------
|
|
res : bool
|
|
"""
|
|
return self._exc_details is None
|
|
|
|
def __str__(self):
|
|
data = (f"{type(self.data).__qualname__}"
|
|
if self.data is not None else "None")
|
|
return f"Event({self._kind}, {self._status}, data: {data})"
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
_registered = defaultdict(list)
|
|
|
|
|
|
def register(kind, listener):
|
|
"""Register a listener for a given event kind.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
listener : Listener
|
|
"""
|
|
assert isinstance(listener, Listener)
|
|
kind = _guard_kind(kind)
|
|
_registered[kind].append(listener)
|
|
|
|
|
|
def unregister(kind, listener):
|
|
"""Unregister a listener for a given event kind.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
listener : Listener
|
|
"""
|
|
assert isinstance(listener, Listener)
|
|
kind = _guard_kind(kind)
|
|
lst = _registered[kind]
|
|
lst.remove(listener)
|
|
|
|
|
|
def broadcast(event):
|
|
"""Broadcast an event to all registered listeners.
|
|
|
|
Parameters
|
|
----------
|
|
event : Event
|
|
"""
|
|
for listener in _registered[event.kind]:
|
|
listener.notify(event)
|
|
|
|
|
|
class Listener(abc.ABC):
|
|
"""Base class for all event listeners.
|
|
"""
|
|
@abc.abstractmethod
|
|
def on_start(self, event):
|
|
"""Called when there is a *START* event.
|
|
|
|
Parameters
|
|
----------
|
|
event : Event
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def on_end(self, event):
|
|
"""Called when there is a *END* event.
|
|
|
|
Parameters
|
|
----------
|
|
event : Event
|
|
"""
|
|
pass
|
|
|
|
def notify(self, event):
|
|
"""Notify this Listener with the given Event.
|
|
|
|
Parameters
|
|
----------
|
|
event : Event
|
|
"""
|
|
if event.is_start:
|
|
self.on_start(event)
|
|
elif event.is_end:
|
|
self.on_end(event)
|
|
else:
|
|
raise AssertionError("unreachable")
|
|
|
|
|
|
class TimingListener(Listener):
|
|
"""A listener that measures the total time spent between *START* and
|
|
*END* events during the time this listener is active.
|
|
"""
|
|
def __init__(self):
|
|
self._depth = 0
|
|
|
|
def on_start(self, event):
|
|
if self._depth == 0:
|
|
self._ts = timer()
|
|
self._depth += 1
|
|
|
|
def on_end(self, event):
|
|
self._depth -= 1
|
|
if self._depth == 0:
|
|
last = getattr(self, "_duration", 0)
|
|
self._duration = (timer() - self._ts) + last
|
|
|
|
@property
|
|
def done(self):
|
|
"""Returns a ``bool`` indicating whether a measurement has been made.
|
|
|
|
When this returns ``False``, the matching event has never fired.
|
|
If and only if this returns ``True``, ``.duration`` can be read without
|
|
error.
|
|
"""
|
|
return hasattr(self, "_duration")
|
|
|
|
@property
|
|
def duration(self):
|
|
"""Returns the measured duration.
|
|
|
|
This may raise ``AttributeError``. Users can use ``.done`` to check
|
|
that a measurement has been made.
|
|
"""
|
|
return self._duration
|
|
|
|
|
|
class RecordingListener(Listener):
|
|
"""A listener that records all events and stores them in the ``.buffer``
|
|
attribute as a list of 2-tuple ``(float, Event)``, where the first element
|
|
is the time the event occurred as returned by ``time.time()`` and the second
|
|
element is the event.
|
|
"""
|
|
def __init__(self):
|
|
self.buffer = []
|
|
|
|
def on_start(self, event):
|
|
self.buffer.append((time.time(), event))
|
|
|
|
def on_end(self, event):
|
|
self.buffer.append((time.time(), event))
|
|
|
|
|
|
@contextmanager
|
|
def install_listener(kind, listener):
|
|
"""Install a listener for event "kind" temporarily within the duration of
|
|
the context.
|
|
|
|
Returns
|
|
-------
|
|
res : Listener
|
|
The *listener* provided.
|
|
|
|
Examples
|
|
--------
|
|
|
|
>>> with install_listener("numba:compile", listener):
|
|
>>> some_code() # listener will be active here.
|
|
>>> other_code() # listener will be unregistered by this point.
|
|
|
|
"""
|
|
register(kind, listener)
|
|
try:
|
|
yield listener
|
|
finally:
|
|
unregister(kind, listener)
|
|
|
|
|
|
@contextmanager
|
|
def install_timer(kind, callback):
|
|
"""Install a TimingListener temporarily to measure the duration of
|
|
an event.
|
|
|
|
If the context completes successfully, the *callback* function is executed.
|
|
The *callback* function is expected to take a float argument for the
|
|
duration in seconds.
|
|
|
|
Returns
|
|
-------
|
|
res : TimingListener
|
|
|
|
Examples
|
|
--------
|
|
|
|
This is equivalent to:
|
|
|
|
>>> with install_listener(kind, TimingListener()) as res:
|
|
>>> ...
|
|
"""
|
|
tl = TimingListener()
|
|
with install_listener(kind, tl):
|
|
yield tl
|
|
|
|
if tl.done:
|
|
callback(tl.duration)
|
|
|
|
|
|
@contextmanager
|
|
def install_recorder(kind):
|
|
"""Install a RecordingListener temporarily to record all events.
|
|
|
|
Once the context is closed, users can use ``RecordingListener.buffer``
|
|
to access the recorded events.
|
|
|
|
Returns
|
|
-------
|
|
res : RecordingListener
|
|
|
|
Examples
|
|
--------
|
|
|
|
This is equivalent to:
|
|
|
|
>>> with install_listener(kind, RecordingListener()) as res:
|
|
>>> ...
|
|
"""
|
|
rl = RecordingListener()
|
|
with install_listener(kind, rl):
|
|
yield rl
|
|
|
|
|
|
def start_event(kind, data=None):
|
|
"""Trigger the start of an event of *kind* with *data*.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
Event kind.
|
|
data : any; optional
|
|
Extra event data.
|
|
"""
|
|
evt = Event(kind=kind, status=EventStatus.START, data=data)
|
|
broadcast(evt)
|
|
|
|
|
|
def end_event(kind, data=None, exc_details=None):
|
|
"""Trigger the end of an event of *kind*, *exc_details*.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
Event kind.
|
|
data : any; optional
|
|
Extra event data.
|
|
exc_details : 3-tuple; optional
|
|
Same 3-tuple for ``__exit__``. Or, ``None`` if no error.
|
|
"""
|
|
evt = Event(
|
|
kind=kind, status=EventStatus.END, data=data, exc_details=exc_details,
|
|
)
|
|
broadcast(evt)
|
|
|
|
|
|
@contextmanager
|
|
def trigger_event(kind, data=None):
|
|
"""A context manager to trigger the start and end events of *kind* with
|
|
*data*. The start event is triggered when entering the context.
|
|
The end event is triggered when exiting the context.
|
|
|
|
Parameters
|
|
----------
|
|
kind : str
|
|
Event kind.
|
|
data : any; optional
|
|
Extra event data.
|
|
"""
|
|
with ExitStack() as scope:
|
|
@scope.push
|
|
def on_exit(*exc_details):
|
|
end_event(kind, data=data, exc_details=exc_details)
|
|
|
|
start_event(kind, data=data)
|
|
yield
|
|
|
|
|
|
def _prepare_chrome_trace_data(listener: RecordingListener):
|
|
"""Prepare events in `listener` for serializing as chrome trace data.
|
|
"""
|
|
# The spec for the trace event format can be found at:
|
|
# https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit # noqa
|
|
# This code only uses the JSON Array Format for simplicity.
|
|
pid = os.getpid()
|
|
tid = threading.get_native_id()
|
|
evs = []
|
|
for ts, rec in listener.buffer:
|
|
data = rec.data
|
|
cat = str(rec.kind)
|
|
ts_scaled = ts * 1_000_000 # scale to microseconds
|
|
ph = 'B' if rec.is_start else 'E'
|
|
name = data['name']
|
|
args = data
|
|
ev = dict(
|
|
cat=cat, pid=pid, tid=tid, ts=ts_scaled, ph=ph, name=name,
|
|
args=args,
|
|
)
|
|
evs.append(ev)
|
|
return evs
|
|
|
|
|
|
def _setup_chrome_trace_exit_handler():
|
|
"""Setup a RecordingListener and an exit handler to write the captured
|
|
events to file.
|
|
"""
|
|
listener = RecordingListener()
|
|
register("numba:run_pass", listener)
|
|
filename = config.CHROME_TRACE
|
|
|
|
@atexit.register
|
|
def _write_chrome_trace():
|
|
# The following output file is not multi-process safe.
|
|
evs = _prepare_chrome_trace_data(listener)
|
|
with open(filename, "w") as out:
|
|
json.dump(evs, out)
|
|
|
|
|
|
if config.CHROME_TRACE:
|
|
_setup_chrome_trace_exit_handler()
|