Skip to content

Commit

Permalink
fast: add emit_fast method for 10x faster emission (without safety …
Browse files Browse the repository at this point in the history
…checks) (#331)

* fast: add emit_fast

* fix benchmark

* add another benchmark

* remove bench from asv

* test cov
  • Loading branch information
tlambert03 authored Nov 4, 2024
1 parent 019232e commit 3778c9f
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 2 deletions.
50 changes: 48 additions & 2 deletions src/psygnal/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,52 @@ def emit(

self._run_emit_loop(args)

def emit_fast(self, *args: Any) -> None:
"""Fast emit without any checks.
This method can be up to 10x faster than `emit()`, but it lacks most of the
features and safety checks of `emit()`. Use with caution. Specifically:
- It does not support `check_nargs` or `check_types`
- It does not use any thread safety locks.
- It is not possible to query the emitter with `Signal.current_emitter()`
- It is not possible to query the sender with `Signal.sender()`
- It does not support "queued" or "latest-only" reemission modes for nested
emits. It will always use "immediate" mode, wherein signals emitted by
callbacks are immediately processed in a deeper emission loop.
It DOES, however, support `paused()` and `blocked()`
Parameters
----------
*args : Any
These arguments will be passed when calling each slot (unless the slot
accepts fewer arguments, in which case extra args will be discarded.)
"""
if self._is_blocked:
return

if self._is_paused:
self._args_queue.append(args)
return

try:
for caller in self._slots:
caller.cb(args)
except RecursionError as e:
raise RecursionError(
f"RecursionError when "
f"emitting signal {self.name!r} with args {args}"
) from e
except EmitLoopError as e: # pragma: no cover
raise e
except Exception as cb_err:
loop_err = EmitLoopError(exc=cb_err, signal=self).with_traceback(
cb_err.__traceback__
)
# this comment will show up in the traceback
raise loop_err from cb_err # emit() call ABOVE || callback error BELOW

def __call__(
self, *args: Any, check_nargs: bool = False, check_types: bool = False
) -> None:
Expand All @@ -1199,9 +1245,9 @@ def _run_emit_loop(self, args: tuple[Any, ...]) -> None:
f"RecursionError when "
f"emitting signal {self.name!r} with args {args}"
) from e
except EmitLoopError as e:
raise e
except Exception as cb_err:
if isinstance(cb_err, EmitLoopError):
raise cb_err
loop_err = EmitLoopError(
exc=cb_err,
signal=self,
Expand Down
6 changes: 6 additions & 0 deletions tests/test_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ def test_emit_time(benchmark: Callable, n_connections: int, callback_type: str)
benchmark(emitter.one_int.emit, 1)


def test_emit_fast(benchmark: Callable) -> None:
emitter = Emitter()
emitter.one_int.connect(one_int)
benchmark(emitter.one_int.emit_fast, 1)


@pytest.mark.benchmark
def test_evented_creation() -> None:
@evented
Expand Down
65 changes: 65 additions & 0 deletions tests/test_psygnal.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,71 @@ def test_basic_signal():
mock.assert_called_once_with(1)


def test_emit_fast():
"""Test emit_fast method."""
emitter = Emitter()
mock = MagicMock()
emitter.one_int.connect(mock)
emitter.one_int.emit_fast(1)
mock.assert_called_once_with(1)
mock.reset_mock()

# calling directly also works
emitter.one_int(1)
mock.assert_called_once_with(1)
mock.reset_mock()

with emitter.one_int.blocked():
emitter.one_int.emit_fast(2)
mock.assert_not_called()

with emitter.one_int.paused():
emitter.one_int.emit_fast(3)
mock.assert_not_called()
emitter.one_int.emit_fast(4)
mock.assert_has_calls([call(3), call(4)])


def test_emit_fast_errors():
emitter = Emitter()
err = ValueError()

@emitter.one_int.connect
def boom(v: int) -> None:
raise err

import re

error_re = re.compile(
"signal 'tests.test_psygnal.Emitter.one_int'" f".*{re.escape(__file__)}",
re.DOTALL,
)
with pytest.raises(EmitLoopError, match=error_re):
emitter.one_int.emit_fast(42)


def test_emit_fast_recursion_errors():
"""Test emit_fast method."""
emitter = Emitter()
emitter.one_int.emit_fast(1)

@emitter.one_int.connect
def callback() -> None:
emitter.one_int.emit(2)

with pytest.raises(RecursionError):
emitter.one_int.emit_fast(3)

emitter.one_int.disconnect(callback)

@emitter.one_int.connect
def callback() -> None:
emitter.one_int.emit_fast(2)

with pytest.raises(RecursionError):
emitter.one_int.emit_fast(3)


def test_decorator():
emitter = Emitter()
err = ValueError()
Expand Down

0 comments on commit 3778c9f

Please sign in to comment.