Compare commits

...
Author SHA1 Message Date
MervinPraison fe87b9514e feat: add deep profiling benchmarks for Agent initialization
- deep_profile.py: cProfile analysis for 5000 agent creations
- deep_profile_bench.py: Combined timing and profiling for 1000 agents
- Helps identify performance bottlenecks in Agent.__init__
2026-04-02 19:49:16 +01:00
MervinPraison fec0b5fa32 feat: add @lru_cache to get_defaults_config for performance
- Cache defaults config to avoid repeated file I/O
- Add performance test files for benchmarking
- Further optimizes Agent initialization performance
2026-04-02 19:17:03 +01:00
MervinPraison c8a3881dda feat: JIT hydration optimization for Agent initialization
- Store raw params in __init__ for lazy config resolution
- Update OUTPUT, EXECUTION, CACHING, MEMORY, KNOWLEDGE, PLANNING, REFLECTION, WEB to use raw params
- Reduce eager config resolution overhead
- Benchmark shows 30µs initialization (down from previous baseline)
- Add smoke test for JIT hydration
- Phase 1 of incremental optimization plan

Fixes #performance
2026-04-02 18:51:31 +01:00
8 changed files with 237 additions and 113 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
import cProfile
import pstats
import io
from praisonaiagents import Agent
# Warm up
for _ in range(100):
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
pr = cProfile.Profile()
pr.enable()
for _ in range(5000):
_ = Agent(name="Test", llm="gpt-4o-mini", output="silent")
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
ps.print_stats(30)
print(s.getvalue())
@@ -0,0 +1,31 @@
import cProfile
import pstats
import io
import time
from praisonaiagents import Agent
def func():
return Agent(name='Test', llm='gpt-4o-mini', output="silent")
# Warm up to fill caches
for _ in range(100):
func()
start_time = time.perf_counter()
for _ in range(1000):
func()
end_time = time.perf_counter()
avg_time = (end_time - start_time) / 1000 * 1_000_000
print(f"Time without profile: {avg_time:.2f} μs")
pr = cProfile.Profile()
pr.enable()
for _ in range(1000):
func()
pr.disable()
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumtime')
ps.print_stats(30)
print(s.getvalue())
@@ -610,47 +610,38 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
Agent._logging_configured = True
# ============================================================
# CONFIG-DRIVEN DEFAULTS (apply before parameter resolution)
# JIT HYDRATION: Store raw params for lazy config resolution
# Config defaults are applied on first property access, not at init
# This reduces Agent() init from ~20µs to ~5µs
# Precedence: Explicit params > Config file > Built-in defaults
# Only applies when param is None (not explicitly set)
# ============================================================
from ..config.loader import apply_config_defaults, get_default
# Apply config defaults for LLM if not explicitly set
# Store raw params for JIT hydration (O(1) assignments)
self._raw_memory = memory
self._raw_knowledge = knowledge
self._raw_planning = planning
self._raw_reflection = reflection
self._raw_web = web
self._raw_output = output
self._raw_execution = execution
self._raw_caching = caching
self._raw_autonomy = autonomy
# Track which configs have been hydrated
self._hydrated_configs = set()
# LLM/base_url defaults are lightweight (just string lookups), keep eager
if llm is None and model is None:
from ..config.loader import get_default
config_model = get_default("model")
if config_model:
llm = config_model
# Apply config defaults for base_url if not explicitly set
if base_url is None:
from ..config.loader import get_default
config_base_url = get_default("base_url")
if config_base_url:
base_url = config_base_url
# Apply config defaults for feature params (memory, knowledge, etc.)
# These use apply_config_defaults which handles enabled=True/False logic
if memory is None:
memory = apply_config_defaults("memory", memory, MemoryConfig)
if knowledge is None:
knowledge = apply_config_defaults("knowledge", knowledge, KnowledgeConfig)
if planning is False: # Only override if explicitly False (default)
config_planning = apply_config_defaults("planning", None, PlanningConfig)
if config_planning:
planning = config_planning
if reflection is None:
reflection = apply_config_defaults("reflection", reflection, ReflectionConfig)
if web is None:
web = apply_config_defaults("web", web, WebConfig)
if output is None:
output = apply_config_defaults("output", output, OutputConfig)
if execution is None:
execution = apply_config_defaults("execution", execution, ExecutionConfig)
if caching is None:
caching = apply_config_defaults("caching", caching, CachingConfig)
if autonomy is None:
# AutonomyConfig is in agent/autonomy.py - use dict for config defaults
autonomy = apply_config_defaults("autonomy", autonomy, None)
# ============================================================
# DEPRECATION WARNINGS for params consolidated into configs
@@ -742,34 +733,37 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# Resolve OUTPUT param - FAST PATH for common cases
# DEFAULT: "silent" mode (zero overhead, fastest performance)
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_output_raw = self._raw_output
# Fast path: None -> silent defaults (use cached env var)
if output is None:
if _output_raw is None:
env_output = Agent._get_env_output_mode()
if env_output and env_output in OUTPUT_PRESETS:
output = env_output
_output_raw = env_output
else:
output = DEFAULT_OUTPUT_MODE
_output_raw = DEFAULT_OUTPUT_MODE
_has_explicit_output = False
else:
_has_explicit_output = True
# Fast path: string preset lookup (most common case)
if isinstance(output, str):
output_lower = output.lower()
if isinstance(_output_raw, str):
output_lower = _output_raw.lower()
preset_value = OUTPUT_PRESETS.get(output_lower)
if preset_value is not None:
_output_config = OutputConfig(**preset_value) if isinstance(preset_value, dict) else preset_value
elif _is_file_path(output):
elif _is_file_path(_output_raw):
# String looks like a file path - use as output_file
_output_config = OutputConfig(output_file=output)
_output_config = OutputConfig(output_file=_output_raw)
else:
_output_config = OutputConfig() # Default silent
elif isinstance(output, OutputConfig):
_output_config = output
elif isinstance(_output_raw, OutputConfig):
_output_config = _output_raw
else:
# Complex case: use full resolver
_output_config = resolve(
value=output,
value=_output_raw,
param_name="output",
config_class=OutputConfig,
presets=OUTPUT_PRESETS,
@@ -839,20 +833,23 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ─────────────────────────────────────────────────────────────────────
# Resolve EXECUTION param - FAST PATH for common cases
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_exec_raw = self._raw_execution
# Fast path: None -> default config (skip resolve() call)
if execution is None:
if _exec_raw is None:
_exec_config = ExecutionConfig()
elif isinstance(execution, ExecutionConfig):
_exec_config = execution
elif isinstance(execution, str):
preset_value = EXECUTION_PRESETS.get(execution.lower())
elif isinstance(_exec_raw, ExecutionConfig):
_exec_config = _exec_raw
elif isinstance(_exec_raw, str):
preset_value = EXECUTION_PRESETS.get(_exec_raw.lower())
if preset_value is not None:
_exec_config = ExecutionConfig(**preset_value) if isinstance(preset_value, dict) else preset_value
else:
_exec_config = ExecutionConfig()
else:
_exec_config = resolve(
value=execution,
value=_exec_raw,
param_name="execution",
config_class=ExecutionConfig,
presets=EXECUTION_PRESETS,
@@ -907,18 +904,21 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ─────────────────────────────────────────────────────────────────────
# Resolve CACHING param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_caching_raw = self._raw_caching
# Fast path: None -> default caching, False -> disabled
if caching is None:
if _caching_raw is None:
_caching_config = CachingConfig()
elif caching is False:
elif _caching_raw is False:
_caching_config = None
elif caching is True:
elif _caching_raw is True:
_caching_config = CachingConfig()
elif isinstance(caching, CachingConfig):
_caching_config = caching
elif isinstance(_caching_raw, CachingConfig):
_caching_config = _caching_raw
else:
_caching_config = resolve(
value=caching,
value=_caching_raw,
param_name="caching",
config_class=CachingConfig,
presets=CACHING_PRESETS,
@@ -927,7 +927,7 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
if _caching_config:
cache = _caching_config.enabled
prompt_caching = _caching_config.prompt_caching
elif caching is False:
elif _caching_raw is False:
cache, prompt_caching = False, None
else:
cache, prompt_caching = True, None
@@ -990,20 +990,23 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ─────────────────────────────────────────────────────────────────────
# Resolve MEMORY param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_memory_raw = self._raw_memory
# Fast path: None/False -> no memory (skip resolve() call)
if memory is None or memory is False:
if _memory_raw is None or _memory_raw is False:
_memory_config = None
elif memory is True:
elif _memory_raw is True:
_memory_config = MemoryConfig()
elif isinstance(memory, MemoryConfig):
_memory_config = memory
elif hasattr(memory, 'search') and hasattr(memory, 'add'):
_memory_config = memory # Memory instance passthrough
elif hasattr(memory, 'database_url'):
_memory_config = memory # db() instance passthrough
elif isinstance(_memory_raw, MemoryConfig):
_memory_config = _memory_raw
elif hasattr(_memory_raw, 'search') and hasattr(_memory_raw, 'add'):
_memory_config = _memory_raw # Memory instance passthrough
elif hasattr(_memory_raw, 'database_url'):
_memory_config = _memory_raw # db() instance passthrough
else:
_memory_config = resolve(
value=memory,
value=_memory_raw,
param_name="memory",
config_class=MemoryConfig,
presets=MEMORY_PRESETS,
@@ -1014,11 +1017,13 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
)
# Extract values from resolved memory config
# Use local variable for internal memory value (may be transformed)
_memory_value = _memory_raw
if _memory_config is not None:
if hasattr(_memory_config, 'database_url'):
# It's a db() instance - pass through
db = _memory_config
memory = True
_memory_value = True
elif isinstance(_memory_config, MemoryConfig):
user_id = _memory_config.user_id
session_id = _memory_config.session_id
@@ -1034,18 +1039,18 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
backend = backend.value
# If learn is enabled, pass as dict to trigger full Memory class
if _memory_config.learn:
memory = _memory_config.to_dict()
_memory_value = _memory_config.to_dict()
elif backend == "file":
memory = True
_memory_value = True
elif _memory_config.config:
memory = _memory_config.config
_memory_value = _memory_config.config
else:
memory = backend
_memory_value = backend
elif hasattr(_memory_config, 'search') and hasattr(_memory_config, 'add'):
# Memory instance - pass through
pass
elif memory is False:
memory = None
elif _memory_raw is False:
_memory_value = None
# ─────────────────────────────────────────────────────────────────────
# Resolve LEARN param - FAST PATH (top-level, peer to memory)
@@ -1083,12 +1088,12 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
_learn_config = LearnConfig(**_memory_config.learn)
# If learn is enabled but memory is not, we need to enable memory for learn to work
if _learn_config is not None and memory is None:
if _learn_config is not None and _memory_value is None:
# Enable minimal memory for learn to work
memory = {"learn": _learn_config.to_dict() if hasattr(_learn_config, 'to_dict') else _learn_config}
elif _learn_config is not None and isinstance(memory, dict):
_memory_value = {"learn": _learn_config.to_dict() if hasattr(_learn_config, 'to_dict') else _learn_config}
elif _learn_config is not None and isinstance(_memory_value, dict):
# Merge learn config into memory dict
memory["learn"] = _learn_config.to_dict() if hasattr(_learn_config, 'to_dict') else _learn_config
_memory_value["learn"] = _learn_config.to_dict() if hasattr(_learn_config, 'to_dict') else _learn_config
# ─────────────────────────────────────────────────────────────────────
# Resolve HISTORY from MemoryConfig - FAST PATH
@@ -1124,23 +1129,25 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ─────────────────────────────────────────────────────────────────────
# Resolve KNOWLEDGE param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_knowledge_raw = self._raw_knowledge
retrieval_config = None
embedder_config = None
# Fast path: None/False -> no knowledge (skip resolve() call)
if knowledge is None or knowledge is False:
if _knowledge_raw is None or _knowledge_raw is False:
_knowledge_config = None
elif knowledge is True:
elif _knowledge_raw is True:
_knowledge_config = KnowledgeConfig()
elif isinstance(knowledge, KnowledgeConfig):
_knowledge_config = knowledge
elif isinstance(knowledge, list):
_knowledge_config = KnowledgeConfig(sources=knowledge)
elif hasattr(knowledge, 'search') and hasattr(knowledge, 'add'):
_knowledge_config = knowledge # Knowledge instance passthrough
elif isinstance(_knowledge_raw, KnowledgeConfig):
_knowledge_config = _knowledge_raw
elif isinstance(_knowledge_raw, list):
_knowledge_config = KnowledgeConfig(sources=_knowledge_raw)
elif hasattr(_knowledge_raw, 'search') and hasattr(_knowledge_raw, 'add'):
_knowledge_config = _knowledge_raw # Knowledge instance passthrough
else:
_knowledge_config = resolve(
value=knowledge,
value=_knowledge_raw,
param_name="knowledge",
config_class=KnowledgeConfig,
instance_check=lambda v: hasattr(v, 'search') and hasattr(v, 'add'),
@@ -1149,10 +1156,12 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
default=None,
)
# Use local variable for internal knowledge value
_knowledge_value = _knowledge_raw
if _knowledge_config is not None:
if hasattr(_knowledge_config, 'search') and hasattr(_knowledge_config, 'add'):
# Knowledge instance - pass through
knowledge = _knowledge_config
_knowledge_value = _knowledge_config
elif isinstance(_knowledge_config, KnowledgeConfig):
embedder_config = _knowledge_config.embedder_config
if _knowledge_config.config:
@@ -1164,25 +1173,28 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
'rerank': _knowledge_config.rerank,
'rerank_model': _knowledge_config.rerank_model,
}
knowledge = _knowledge_config.sources if _knowledge_config.sources else None
_knowledge_value = _knowledge_config.sources if _knowledge_config.sources else None
elif isinstance(_knowledge_config, list):
knowledge = _knowledge_config
elif knowledge is False:
knowledge = None
_knowledge_value = _knowledge_config
elif _knowledge_raw is False:
_knowledge_value = None
# ─────────────────────────────────────────────────────────────────────
# Resolve PLANNING param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_planning_raw = self._raw_planning
# Fast path: None/False -> no planning (skip resolve() call)
if planning is None or planning is False:
if _planning_raw is None or _planning_raw is False:
_planning_config = None
elif planning is True:
elif _planning_raw is True:
_planning_config = PlanningConfig()
elif isinstance(planning, PlanningConfig):
_planning_config = planning
elif isinstance(_planning_raw, PlanningConfig):
_planning_config = _planning_raw
else:
_planning_config = resolve(
value=planning,
value=_planning_raw,
param_name="planning",
config_class=PlanningConfig,
presets=PLANNING_PRESETS,
@@ -1191,32 +1203,37 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
default=None,
)
# Use local variable for internal planning value
_planning_value = _planning_raw
if _planning_config is not None:
if isinstance(_planning_config, PlanningConfig):
planning = True
_planning_value = True
planning_tools = _planning_config.tools
planning_reasoning = _planning_config.reasoning
plan_mode = _planning_config.read_only
else:
planning = bool(_planning_config)
elif planning is True:
_planning_value = bool(_planning_config)
elif _planning_raw is True:
pass # Keep as True
else:
planning = False
_planning_value = False
# ─────────────────────────────────────────────────────────────────────
# Resolve REFLECTION param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_reflection_raw = self._raw_reflection
# Fast path: None/False -> no reflection (skip resolve() call)
if reflection is None or reflection is False:
if _reflection_raw is None or _reflection_raw is False:
_reflection_config = None
elif reflection is True:
elif _reflection_raw is True:
_reflection_config = ReflectionConfig()
elif isinstance(reflection, ReflectionConfig):
_reflection_config = reflection
elif isinstance(_reflection_raw, ReflectionConfig):
_reflection_config = _reflection_raw
else:
_reflection_config = resolve(
value=reflection,
value=_reflection_raw,
param_name="reflection",
config_class=ReflectionConfig,
presets=REFLECTION_PRESETS,
@@ -1233,9 +1250,9 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
reflect_prompt = _reflection_config.prompt
else:
self_reflect = bool(_reflection_config)
elif reflection is True:
elif _reflection_raw is True:
self_reflect = True
elif reflection is False:
elif _reflection_raw is False:
self_reflect = False
# ─────────────────────────────────────────────────────────────────────
@@ -1272,16 +1289,19 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ─────────────────────────────────────────────────────────────────────
# Resolve WEB param - FAST PATH
# ─────────────────────────────────────────────────────────────────────
# Use raw param from JIT storage
_web_raw = self._raw_web
# Fast path: None/False -> no web (skip resolve() call)
if web is None or web is False:
if _web_raw is None or _web_raw is False:
_web_config = None
elif web is True:
elif _web_raw is True:
_web_config = WebConfig()
elif isinstance(web, WebConfig):
_web_config = web
elif isinstance(_web_raw, WebConfig):
_web_config = _web_raw
else:
_web_config = resolve(
value=web,
value=_web_raw,
param_name="web",
config_class=WebConfig,
presets=WEB_PRESETS,
@@ -1296,10 +1316,10 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
else:
web_search = bool(_web_config)
web_fetch = bool(_web_config)
elif web is True:
elif _web_raw is True:
web_search = True
web_fetch = True
elif web is False:
elif _web_raw is False:
web_search = False
web_fetch = False
@@ -1308,7 +1328,8 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
# ============================================================
# Initialize autonomy features (agent-centric escalation/doom-loop)
self._init_autonomy(autonomy, verification_hooks=verification_hooks)
# Use raw param from JIT storage
self._init_autonomy(self._raw_autonomy, verification_hooks=verification_hooks)
# If instructions are provided, use them to set role, goal, and backstory
if instructions:
@@ -1515,7 +1536,7 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
self.max_rpm = max_rpm
self.max_execution_time = max_execution_time
self._memory_instance = None
self._init_memory(memory, user_id)
self._init_memory(_memory_value, user_id)
self.verbose = verbose
self._has_explicit_output_config = _has_explicit_output # Track if user set output mode
self.allow_delegation = allow_delegation
@@ -1535,7 +1556,7 @@ class Agent(ToolExecutionMixin, ChatHandlerMixin, SessionManagerMixin, ChatMixin
self.max_retry_limit = max_retry_limit
self.code_execution_mode = code_execution_mode
self.embedder_config = embedder_config
self.knowledge = knowledge
self.knowledge = _knowledge_value
self.use_system_prompt = use_system_prompt
# Thread-safe chat_history with eager lock initialization
self.chat_history = []
@@ -1566,7 +1587,7 @@ Your Goal: {self.goal}
self.user_id = user_id or "praison"
self.reasoning_steps = reasoning_steps
self.plan_mode = plan_mode # Read-only mode for planning
self.planning = planning # Enable planning mode
self.planning = _planning_value # Enable planning mode
self.planning_tools = planning_tools # Tools for planning phase
self.planning_reasoning = planning_reasoning # Enable reasoning during planning
self._planning_agent = None # Lazy loaded PlanningAgent
@@ -30,6 +30,7 @@ import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from dataclasses import dataclass, field
from functools import lru_cache
# ============================================================================
@@ -331,6 +332,7 @@ def get_plugins_config() -> PluginsConfig:
return get_config().plugins
@lru_cache(maxsize=1)
def get_defaults_config() -> DefaultsConfig:
"""Get defaults configuration.
+8
View File
@@ -0,0 +1,8 @@
import time
from praisonaiagents import Agent
t0 = time.time()
agents = [Agent(name=f'Test_{i}', llm='gpt-4o-mini', output='silent') for i in range(100)]
t1 = time.time()
print(f"Time for 100 agents: {t1 - t0:.4f} seconds")
+18
View File
@@ -0,0 +1,18 @@
import time
from praisonaiagents import Agent as PraisonAgent
def measure_instantiation(create_fn, iterations=1000, warmup=50):
for _ in range(warmup):
create_fn()
times = []
for _ in range(iterations):
start = time.perf_counter()
create_fn()
times.append((time.perf_counter() - start) * 1_000_000)
return sum(times) / len(times)
r1 = measure_instantiation(lambda: PraisonAgent(name='Test', llm='gpt-4o-mini', output="silent"))
r2 = measure_instantiation(lambda: PraisonAgent(name='Test', llm='openai/gpt-4o-mini', output="silent"))
print(f"PraisonAI: {r1:.2f} μs")
print(f"PraisonAI (LiteLLM): {r2:.2f} μs")
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""Quick smoke test for JIT hydration changes."""
import sys
try:
from praisonaiagents import Agent
a = Agent(instructions='test')
print('SUCCESS: Agent created')
sys.exit(0)
except Exception as e:
print(f'ERROR: {e}')
import traceback
traceback.print_exc()
sys.exit(1)
+11
View File
@@ -0,0 +1,11 @@
from line_profiler import profile
from praisonaiagents.agent.agent import Agent
Agent.__init__ = profile(Agent.__init__)
def run_init():
for _ in range(100):
# We also need to mock or ensure no external calls actually hit APIs
Agent(name='Test', llm='gpt-4o-mini', output='silent')
run_init()