mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
fix: Core SDK: EventBus does eager work (uuid4 + lock + history) on every publish even with zero subscribers (#2066)
* perf: optimize EventBus to skip expensive work when no subscribers - Add fast-path check in publish_event() and publish_async() to skip lock, history append, and Event creation when no subscribers exist - Add has_subscribers property for efficient lock-free checking - Update memory _emit_memory_event() to conditionally build payload only when subscribers exist, avoiding datetime.now() and dict construction - Preserve identical behavior when subscribers are present - Add comprehensive tests for optimization behavior Fixes hot-path performance issue in memory operations and sub-agent lifecycle events where uuid4() + time.time() + lock + history append occurred on every publish even with zero subscribers. Fixes #1954 Co-authored-by: MervinPraison <[email protected]> * fix: address reviewer feedback for EventBus optimization - Move fast-path check before Event construction in publish() and publish_async() to avoid uuid4/time calls - Eliminate event_data dict duplication in memory/core.py using lazy evaluation helper - Remove unused mock patch from test - Remove duplicate test file from repository root Co-authored-by: Mervin Praison <[email protected]> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: MervinPraison <[email protected]>
This commit is contained in:
3 files changed
+105
-12
No files matched your search
@@ -170,6 +170,17 @@ class EventBus:
|
||||
Returns:
|
||||
The published Event object
|
||||
"""
|
||||
# Fast path: if no subscribers, return a minimal event without expensive operations
|
||||
if not self._subscribers:
|
||||
# Convert EventType enum to string
|
||||
type_str = event_type.value if isinstance(event_type, EventType) else event_type
|
||||
return Event(
|
||||
type=type_str,
|
||||
data=data or {},
|
||||
source=source,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
# Convert EventType enum to string
|
||||
type_str = event_type.value if isinstance(event_type, EventType) else event_type
|
||||
|
||||
@@ -194,6 +205,10 @@ class EventBus:
|
||||
"""
|
||||
logger.debug(f"Publishing event: {event.type}")
|
||||
|
||||
# Fast path: if no subscribers, skip expensive work
|
||||
if not self._subscribers:
|
||||
return event
|
||||
|
||||
# Store in history
|
||||
with self._lock:
|
||||
self._event_history.append(event)
|
||||
@@ -245,6 +260,15 @@ class EventBus:
|
||||
"""
|
||||
type_str = event_type.value if isinstance(event_type, EventType) else event_type
|
||||
|
||||
# Fast path: if no subscribers, return a minimal event without expensive operations
|
||||
if not self._subscribers:
|
||||
return Event(
|
||||
type=type_str,
|
||||
data=data or {},
|
||||
source=source,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
event = Event(
|
||||
type=type_str,
|
||||
data=data or {},
|
||||
@@ -321,6 +345,11 @@ class EventBus:
|
||||
with self._lock:
|
||||
return len(self._subscribers)
|
||||
|
||||
@property
|
||||
def has_subscribers(self) -> bool:
|
||||
"""Check if there are any subscribers (lock-free for performance)."""
|
||||
return bool(self._subscribers)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EventBus(subscribers={self.subscriber_count})"
|
||||
|
||||
|
||||
@@ -378,27 +378,34 @@ class MemoryCoreMixin:
|
||||
def _emit_memory_event(self, event_type: str, memory_type: str, content: str, metadata: Dict):
|
||||
"""Emit a memory-related event for monitoring/hooks."""
|
||||
try:
|
||||
# Integrate with the existing event system
|
||||
event_data = {
|
||||
'type': event_type,
|
||||
'memory_type': memory_type,
|
||||
'content_preview': content[:100] + ('...' if len(content) > 100 else ''),
|
||||
'metadata': metadata,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'provider': getattr(self, 'provider', 'unknown')
|
||||
}
|
||||
# Helper to build event data lazily
|
||||
def _build_event_data():
|
||||
return {
|
||||
'type': event_type,
|
||||
'memory_type': memory_type,
|
||||
'content_preview': content[:100] + ('...' if len(content) > 100 else ''),
|
||||
'metadata': metadata,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'provider': getattr(self, 'provider', 'unknown')
|
||||
}
|
||||
|
||||
# Try to emit to the EventBus if available
|
||||
try:
|
||||
from ..bus import get_default_bus
|
||||
bus = get_default_bus()
|
||||
bus.publish(f"memory.{event_type}.{memory_type}", event_data)
|
||||
|
||||
# Fast path: only build expensive payload if someone is listening
|
||||
if not bus.has_subscribers:
|
||||
return
|
||||
|
||||
# Integrate with the existing event system
|
||||
bus.publish(f"memory.{event_type}.{memory_type}", _build_event_data())
|
||||
except ImportError:
|
||||
# EventBus not available, fall back to logging
|
||||
logging.debug(f"Memory event: {event_type}.{memory_type} - {event_data}")
|
||||
logging.debug(f"Memory event: {event_type}.{memory_type} - {_build_event_data()}")
|
||||
except Exception as bus_error:
|
||||
# EventBus failed, log the event instead
|
||||
logging.debug(f"Memory event (bus failed): {event_type}.{memory_type} - {event_data}")
|
||||
logging.debug(f"Memory event (bus failed): {event_type}.{memory_type} - {_build_event_data()}")
|
||||
logging.debug(f"EventBus error: {bus_error}")
|
||||
except Exception as e:
|
||||
logging.debug(f"Failed to emit memory event: {e}")
|
||||
|
||||
@@ -360,6 +360,63 @@ class TestDefaultBus:
|
||||
set_default_bus(original)
|
||||
|
||||
|
||||
class TestEventBusOptimization:
|
||||
"""Tests for EventBus performance optimizations."""
|
||||
|
||||
def test_has_subscribers_property(self):
|
||||
"""Test has_subscribers property."""
|
||||
bus = EventBus()
|
||||
|
||||
# Should be False when empty
|
||||
assert not bus.has_subscribers
|
||||
|
||||
# Add a subscriber
|
||||
bus.subscribe(lambda e: None)
|
||||
assert bus.has_subscribers
|
||||
|
||||
# Remove subscriber
|
||||
bus.clear_subscribers()
|
||||
assert not bus.has_subscribers
|
||||
|
||||
def test_publish_fast_path_no_subscribers(self):
|
||||
"""Test that publishing with no subscribers skips expensive work."""
|
||||
bus = EventBus()
|
||||
|
||||
# Create a real event and publish directly
|
||||
real_event = Event(type="test.event", data={})
|
||||
result = bus.publish_event(real_event)
|
||||
|
||||
# Should return the event without storing in history
|
||||
assert result is real_event
|
||||
assert len(bus.get_history()) == 0 # No history when no subscribers
|
||||
|
||||
def test_publish_normal_path_with_subscribers(self):
|
||||
"""Test that publishing with subscribers works normally."""
|
||||
bus = EventBus()
|
||||
received = []
|
||||
|
||||
bus.subscribe(lambda e: received.append(e))
|
||||
|
||||
event = Event(type="test.event", data={"key": "value"})
|
||||
result = bus.publish_event(event)
|
||||
|
||||
# Should store in history and call subscribers
|
||||
assert result is event
|
||||
assert len(bus.get_history()) == 1
|
||||
assert len(received) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_publish_async_fast_path_no_subscribers(self):
|
||||
"""Test that async publishing with no subscribers skips expensive work."""
|
||||
bus = EventBus()
|
||||
|
||||
result = await bus.publish_async("test.event", {"key": "value"})
|
||||
|
||||
# Should return event but not store in history
|
||||
assert isinstance(result, Event)
|
||||
assert len(bus.get_history()) == 0 # No history when no subscribers
|
||||
|
||||
|
||||
class TestEventType:
|
||||
"""Tests for EventType enum."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user