Skip to content

Commit 65e46e4

Browse files
committed
perf: replace logger.debug with cached no-op to eliminate logging overhead
logger.debug() consumed 65-75% of event processing time even with DEBUG disabled, due to frame inspection, string formatting, and lock acquisition on every call. - Store `self._debug` on BaseEngine at init: real logger.debug when DEBUG is enabled, no-op lambda otherwise - Add BaseEngine.stop() to encapsulate external engine shutdown (replaces _stop_child_machine's inline engine manipulation) - InvokeManager delegates _debug/_log_id to engine for consistent prefixes - Module-level _debug in io/scxml/actions.py for parse-time callables - Remove unused logging imports from sync/async engines and invoke module
1 parent 4a2ac88 commit 65e46e4

6 files changed

Lines changed: 77 additions & 62 deletions

File tree

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,14 @@ disable_error_code = "annotation-unchecked"
131131
mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/django_project"
132132

133133
[[tool.mypy.overrides]]
134-
module = ['django.*', 'pytest.*', 'pydot.*', 'sphinx_gallery.*', 'docutils.*', 'sphinx.*']
134+
module = [
135+
'django.*',
136+
'pytest.*',
137+
'pydot.*',
138+
'sphinx_gallery.*',
139+
'docutils.*',
140+
'sphinx.*',
141+
]
135142
ignore_missing_imports = true
136143

137144
[tool.ruff]

statemachine/engines/async_.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import contextvars
3-
import logging
43
from itertools import chain
54
from time import time
65
from typing import TYPE_CHECKING
@@ -20,9 +19,6 @@
2019
from ..event import Event
2120
from ..transition import Transition
2221

23-
logger = logging.getLogger(__name__)
24-
25-
2622
# ContextVar to distinguish reentrant calls (from within callbacks) from
2723
# concurrent external calls. asyncio propagates context to child tasks
2824
# (e.g., those created by asyncio.gather in the callback system), so a
@@ -179,7 +175,7 @@ async def _exit_states( # type: ignore[override]
179175
args, kwargs = await self._get_args_kwargs(info.transition, trigger_data)
180176

181177
if info.state is not None: # pragma: no branch
182-
logger.debug("%s Exiting state: %s", self._log_id, info.state)
178+
self._debug("%s Exiting state: %s", self._log_id, info.state)
183179
await self.sm._callbacks.async_call(
184180
info.state.exit.key, *args, on_error=on_error, **kwargs
185181
)
@@ -234,7 +230,7 @@ async def _enter_states( # noqa: C901
234230
target=target,
235231
)
236232

237-
logger.debug("%s Entering state: %s", self._log_id, target)
233+
self._debug("%s Entering state: %s", self._log_id, target)
238234
self._add_state_to_configuration(target)
239235

240236
on_entry_result = await self.sm._callbacks.async_call(
@@ -274,7 +270,7 @@ async def _enter_states( # noqa: C901
274270

275271
async def microstep(self, transitions: "List[Transition]", trigger_data: TriggerData):
276272
self._microstep_count += 1
277-
logger.debug(
273+
self._debug(
278274
"%s macro:%d micro:%d transitions: %s",
279275
self._log_id,
280276
self._macrostep_count,
@@ -366,7 +362,7 @@ async def processing_loop( # noqa: C901
366362
return None
367363

368364
_ctx_token = _in_processing_loop.set(True)
369-
logger.debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
365+
self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
370366
first_result = self._sentinel
371367
try:
372368
took_events = True
@@ -378,7 +374,7 @@ async def processing_loop( # noqa: C901
378374
# Phase 1: eventless transitions and internal events
379375
while not macrostep_done:
380376
self._microstep_count = 0
381-
logger.debug(
377+
self._debug(
382378
"%s Macrostep %d: eventless/internal queue",
383379
self._log_id,
384380
self._macrostep_count,
@@ -394,7 +390,7 @@ async def processing_loop( # noqa: C901
394390
internal_event = self.internal_queue.pop()
395391
enabled_transitions = await self.select_transitions(internal_event)
396392
if enabled_transitions:
397-
logger.debug(
393+
self._debug(
398394
"%s Enabled transitions: %s", self._log_id, enabled_transitions
399395
)
400396
took_events = True
@@ -412,9 +408,7 @@ async def processing_loop( # noqa: C901
412408
await self._run_microstep(enabled_transitions, internal_event)
413409

414410
# Phase 3: external events
415-
logger.debug(
416-
"%s Macrostep %d: external queue", self._log_id, self._macrostep_count
417-
)
411+
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
418412
while not self.external_queue.is_empty():
419413
self.clear_cache()
420414
took_events = True
@@ -429,7 +423,7 @@ async def processing_loop( # noqa: C901
429423

430424
self._macrostep_count += 1
431425
self._microstep_count = 0
432-
logger.debug(
426+
self._debug(
433427
"%s macrostep %d: event=%s",
434428
self._log_id,
435429
self._macrostep_count,
@@ -453,7 +447,7 @@ async def processing_loop( # noqa: C901
453447
event_future = external_event.future
454448
try:
455449
enabled_transitions = await self.select_transitions(external_event)
456-
logger.debug(
450+
self._debug(
457451
"%s Enabled transitions: %s", self._log_id, enabled_transitions
458452
)
459453
if enabled_transitions:
@@ -494,7 +488,7 @@ async def processing_loop( # noqa: C901
494488
_in_processing_loop.reset(_ctx_token)
495489
self._processing.release()
496490

497-
logger.debug("%s Processing loop ended", self._log_id)
491+
self._debug("%s Processing loop ended", self._log_id)
498492
result = first_result if first_result is not self._sentinel else None
499493
# If the caller has a future, await it (already resolved by now).
500494
if caller_future is not None:

statemachine/engines/base.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def __init__(self, sm: "StateChart"):
9999
self._macrostep_count: int = 0
100100
self._microstep_count: int = 0
101101
self._log_id = f"[{type(sm).__name__}]"
102+
self._debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None
102103
self._root_parallel_final_pending: "State | None" = None
103104

104105
def empty(self): # pragma: no cover
@@ -125,7 +126,7 @@ def put(self, trigger_data: TriggerData, internal: bool = False, _delayed: bool
125126
self.external_queue.put(trigger_data)
126127

127128
if not _delayed:
128-
logger.debug(
129+
self._debug(
129130
"%s New event '%s' put on the '%s' queue",
130131
self._log_id,
131132
trigger_data.event,
@@ -180,7 +181,7 @@ def _send_error_execution(self, error: Exception, trigger_data: TriggerData):
180181
181182
If already processing an error.execution event, ignore to avoid infinite loops.
182183
"""
183-
logger.debug(
184+
self._debug(
184185
"%s Error %s captured while executing event=%s",
185186
self._log_id,
186187
error,
@@ -382,7 +383,7 @@ def microstep(self, transitions: List[Transition], trigger_data: TriggerData):
382383
This includes exiting states, executing transition content, and entering states.
383384
"""
384385
self._microstep_count += 1
385-
logger.debug(
386+
self._debug(
386387
"%s macro:%d micro:%d transitions: %s",
387388
self._log_id,
388389
self._macrostep_count,
@@ -469,7 +470,7 @@ def _prepare_exit_states(
469470
states_to_exit, key=lambda x: x.state and x.state.document_order or 0, reverse=True
470471
)
471472
result = OrderedSet([info.state for info in ordered_states if info.state])
472-
logger.debug("%s States to exit: %s", self._log_id, result)
473+
self._debug("%s States to exit: %s", self._log_id, result)
473474

474475
# Update history
475476
for info in ordered_states:
@@ -480,7 +481,7 @@ def _prepare_exit_states(
480481
else: # shallow history
481482
history_value = [s for s in self.sm.configuration if s.parent == state]
482483

483-
logger.debug(
484+
self._debug(
484485
"%s Saving '%s.%s' history state: '%s'",
485486
self._log_id,
486487
state,
@@ -512,7 +513,7 @@ def _exit_states(
512513

513514
# Execute `onexit` handlers — same per-block error isolation as onentry.
514515
if info.state is not None: # pragma: no branch
515-
logger.debug("%s Exiting state: %s", self._log_id, info.state)
516+
self._debug("%s Exiting state: %s", self._log_id, info.state)
516517
self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs)
517518

518519
self._remove_state_from_configuration(info.state)
@@ -569,7 +570,7 @@ def _prepare_entry_states(
569570
new_configuration = cast(
570571
OrderedSet[State], (previous_configuration - states_to_exit) | states_targets_to_enter
571572
)
572-
logger.debug("%s States to enter: %s", self._log_id, states_targets_to_enter)
573+
self._debug("%s States to enter: %s", self._log_id, states_targets_to_enter)
573574

574575
return ordered_states, states_for_default_entry, default_history_content, new_configuration
575576

@@ -578,6 +579,15 @@ def _add_state_to_configuration(self, target: State):
578579
if not self.sm.atomic_configuration_update:
579580
self.sm._config.add(target)
580581

582+
def stop(self):
583+
"""Stop this engine externally (e.g. when a parent cancels a child invocation)."""
584+
self._debug("%s Stopping engine", self._log_id)
585+
self.running = False
586+
try:
587+
self._invoke_manager.cancel_all()
588+
except Exception: # pragma: no cover
589+
self._debug("%s Error stopping engine", self._log_id, exc_info=True)
590+
581591
def __del__(self):
582592
try:
583593
self._invoke_manager.cancel_all()
@@ -586,7 +596,7 @@ def __del__(self):
586596

587597
def _handle_final_state(self, target: State, on_entry_result: list):
588598
"""Handle final state entry: queue done events. No direct callback dispatch."""
589-
logger.debug("%s Reached final state: %s", self._log_id, target)
599+
self._debug("%s Reached final state: %s", self._log_id, target)
590600
if target.parent is None:
591601
self._invoke_manager.cancel_all()
592602
self.running = False
@@ -665,7 +675,7 @@ def _enter_states( # noqa: C901
665675
target=target,
666676
)
667677

668-
logger.debug("%s Entering state: %s", self._log_id, target)
678+
self._debug("%s Entering state: %s", self._log_id, target)
669679
self._add_state_to_configuration(target)
670680

671681
# Execute `onentry` handlers — each handler is a separate block per
@@ -765,7 +775,7 @@ def add_descendant_states_to_enter( # noqa: C901
765775
parent_id = state.parent and state.parent.id
766776
default_history_content[parent_id] = [info]
767777
if state.id in self.sm.history_values:
768-
logger.debug(
778+
self._debug(
769779
"%s History state '%s.%s' %s restoring: '%s'",
770780
self._log_id,
771781
state.parent,
@@ -795,7 +805,7 @@ def add_descendant_states_to_enter( # noqa: C901
795805
)
796806
else:
797807
# Handle default history content
798-
logger.debug(
808+
self._debug(
799809
"%s History state '%s.%s' default content: %s",
800810
self._log_id,
801811
state.parent,

statemachine/engines/sync.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
from time import sleep
32
from time import time
43
from typing import TYPE_CHECKING
@@ -14,8 +13,6 @@
1413
if TYPE_CHECKING:
1514
from ..transition import Transition
1615

17-
logger = logging.getLogger(__name__)
18-
1916

2017
class SyncEngine(BaseEngine):
2118
def _run_microstep(self, enabled_transitions, trigger_data):
@@ -77,7 +74,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
7774
# We will collect the first result as the processing result to keep backwards compatibility
7875
# so we need to use a sentinel object instead of `None` because the first result may
7976
# be also `None`, and on this case the `first_result` may be overridden by another result.
80-
logger.debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
77+
self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
8178
first_result = self._sentinel
8279
try:
8380
took_events = True
@@ -92,7 +89,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
9289
# handles eventless transitions and internal events
9390
while not macrostep_done:
9491
self._microstep_count = 0
95-
logger.debug(
92+
self._debug(
9693
"%s Macrostep %d: eventless/internal queue",
9794
self._log_id,
9895
self._macrostep_count,
@@ -110,7 +107,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
110107
internal_event = self.internal_queue.pop()
111108
enabled_transitions = self.select_transitions(internal_event)
112109
if enabled_transitions:
113-
logger.debug(
110+
self._debug(
114111
"%s Enabled transitions: %s", self._log_id, enabled_transitions
115112
)
116113
took_events = True
@@ -130,9 +127,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
130127
self._run_microstep(enabled_transitions, internal_event)
131128

132129
# Process external events
133-
logger.debug(
134-
"%s Macrostep %d: external queue", self._log_id, self._macrostep_count
135-
)
130+
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
136131
while not self.external_queue.is_empty():
137132
self.clear_cache()
138133
took_events = True
@@ -147,7 +142,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
147142

148143
self._macrostep_count += 1
149144
self._microstep_count = 0
150-
logger.debug(
145+
self._debug(
151146
"%s macrostep %d: event=%s",
152147
self._log_id,
153148
self._macrostep_count,
@@ -158,7 +153,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
158153
self._invoke_manager.handle_external_event(external_event)
159154

160155
enabled_transitions = self.select_transitions(external_event)
161-
logger.debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
156+
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
162157
if enabled_transitions:
163158
try:
164159
result = self.microstep(list(enabled_transitions), external_event)
@@ -177,7 +172,7 @@ def processing_loop(self, caller_future=None): # noqa: C901
177172

178173
finally:
179174
self._processing.release()
180-
logger.debug("%s Processing loop ended", self._log_id)
175+
self._debug("%s Processing loop ended", self._log_id)
181176
return first_result if first_result is not self._sentinel else None
182177

183178
def enabled_events(self, *args, **kwargs):

0 commit comments

Comments
 (0)