mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-05 01:02:37 +00:00
perf: lazy-load MCP SDK and event types to reduce cold start by ~29% (#5584)
* perf: defer MCP SDK import by fixing import path in agent/core.py - Change 'from crewai.mcp import MCPServerConfig' to direct path 'from crewai.mcp.config import MCPServerConfig' to avoid triggering mcp/__init__.py which eagerly loads the full mcp SDK (~300-400ms) - Move MCPToolResolver import into get_mcp_tools() method body since it's only used at runtime, not in type annotations Saves ~200ms on 'import crewai' cold start. * perf: lazy-load heavy MCP imports in mcp/__init__.py MCPClient, MCPToolResolver, BaseTransport, and TransportType now use __getattr__ lazy loading. These pull in the full mcp SDK (~400ms) but are only needed at runtime when agents actually connect to MCP servers. Lightweight config and filter types remain eagerly imported. * perf: lazy-load all event type modules in events/__init__.py Previously only agent_events were lazy-loaded; all other event type modules (crew, flow, knowledge, llm, guardrail, logging, mcp, memory, reasoning, skill, task, tool_usage) were eagerly imported at package init time. Since events/__init__.py runs whenever ANY crewai.events.* submodule is accessed, this loaded ~12 Pydantic model modules unnecessarily. Now all event types use the same __getattr__ lazy-loading pattern, with TYPE_CHECKING imports preserved for IDE/type-checker support. Saves ~550ms on 'import crewai' cold start. * chore: remove UNKNOWN.egg-info from version control * fix: add MCPToolResolver to TYPE_CHECKING imports Fixes F821 (ruff) and name-defined (mypy) from lazy-loading the MCP import. The type annotation on _mcp_resolver needs the name available at type-check time. * fix: bump lxml to >=5.4.0 for GHSA-vfmq-68hx-4jfw lxml 5.3.2 has a known vulnerability. Bump to 5.4.0+ which includes the fix (libxml2 2.13.8). The previous <5.4.0 pin was for etree import issues that have since been resolved. * fix: bump exclude-newer to 2026-04-22 for lxml 6.1.0 resolution lxml 6.1.0 (GHSA fix) was released April 17 but the exclude-newer date was set to April 17, missing it by timestamp. Bump to April 22. * perf: add import time benchmark script scripts/benchmark_import_time.py measures import crewai cold start in fresh subprocesses. Supports --runs, --json (for CI), and --threshold (fail if median exceeds N seconds). The companion GitHub Action workflow needs to be pushed separately (requires workflow scope). * new action * Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Joao Moura <joaomdmoura@gmail.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
76
scripts/benchmark_import_time.py
Executable file
76
scripts/benchmark_import_time.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Benchmark `import crewai` cold start time.
|
||||
|
||||
Usage:
|
||||
python scripts/benchmark_import_time.py [--runs N] [--json]
|
||||
|
||||
Spawns a fresh Python subprocess for each run to ensure cold imports.
|
||||
Prints median, mean, min, max across all runs.
|
||||
With --json, outputs machine-readable results for CI.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
IMPORT_SCRIPT = "import time; t0 = time.perf_counter(); import crewai; print(time.perf_counter() - t0)"
|
||||
|
||||
|
||||
def measure_import(python: str = sys.executable) -> float:
|
||||
"""Run a single cold-import measurement in a subprocess."""
|
||||
result = subprocess.run(
|
||||
[python, "-c", IMPORT_SCRIPT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={"PATH": "", "VIRTUAL_ENV": "", "PYTHONPATH": ""},
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Import failed: {result.stderr.strip()}")
|
||||
return float(result.stdout.strip())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Benchmark crewai import time")
|
||||
parser.add_argument("--runs", type=int, default=5, help="Number of runs (default: 5)")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON for CI")
|
||||
parser.add_argument("--threshold", type=float, default=None,
|
||||
help="Fail if median exceeds this value (seconds)")
|
||||
args = parser.parse_args()
|
||||
|
||||
times = []
|
||||
for i in range(args.runs):
|
||||
t = measure_import()
|
||||
times.append(t)
|
||||
if not args.json:
|
||||
print(f" Run {i + 1}: {t:.3f}s")
|
||||
|
||||
median = statistics.median(times)
|
||||
mean = statistics.mean(times)
|
||||
stdev = statistics.stdev(times) if len(times) > 1 else 0.0
|
||||
|
||||
result = {
|
||||
"runs": args.runs,
|
||||
"median_s": round(median, 3),
|
||||
"mean_s": round(mean, 3),
|
||||
"stdev_s": round(stdev, 3),
|
||||
"min_s": round(min(times), 3),
|
||||
"max_s": round(max(times), 3),
|
||||
}
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(f"\n Median: {median:.3f}s")
|
||||
print(f" Mean: {mean:.3f}s ± {stdev:.3f}s")
|
||||
print(f" Range: {min(times):.3f}s – {max(times):.3f}s")
|
||||
|
||||
if args.threshold and median > args.threshold:
|
||||
print(f"\n ❌ FAILED: median {median:.3f}s exceeds threshold {args.threshold:.3f}s")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user