Files
crewAI/scripts/benchmark_import_time.py
iris-clawd 3be2fb65dc 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>
2026-04-22 02:17:33 -03:00

77 lines
2.3 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()