mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-04 16:52: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:
103
.github/workflows/import-time.yml
vendored
Normal file
103
.github/workflows/import-time.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: Import Time Guard
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "lib/crewai/src/**"
|
||||
- "lib/crewai/pyproject.toml"
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
import-time:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "0.11.3"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --no-dev
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
|
||||
- name: Benchmark PR branch
|
||||
id: pr
|
||||
run: |
|
||||
result=$(uv run python scripts/benchmark_import_time.py --runs 5 --json)
|
||||
echo "result=$result" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_median=$(echo $result | python3 -c 'import sys,json; print(json.load(sys.stdin)["median_s"])')" >> "$GITHUB_OUTPUT"
|
||||
echo "### PR Branch Import Time" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "$result" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(f'- Median: {d[\"median_s\"]}s')
|
||||
print(f'- Mean: {d[\"mean_s\"]}s ± {d[\"stdev_s\"]}s')
|
||||
print(f'- Range: {d[\"min_s\"]}s – {d[\"max_s\"]}s')
|
||||
" >> "$GITHUB_STEP_SUMMARY"
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
|
||||
- name: Checkout base branch
|
||||
run: git checkout ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Install base branch
|
||||
run: uv sync --all-extras --no-dev
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
|
||||
- name: Benchmark base branch
|
||||
id: base
|
||||
run: |
|
||||
result=$(uv run python scripts/benchmark_import_time.py --runs 5 --json 2>/dev/null || echo '{"median_s": 0}')
|
||||
echo "result=$result" >> "$GITHUB_OUTPUT"
|
||||
echo "base_median=$(echo $result | python3 -c 'import sys,json; print(json.load(sys.stdin)["median_s"])')" >> "$GITHUB_OUTPUT"
|
||||
echo "### Base Branch Import Time" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "$result" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
if d.get('median_s', 0) > 0:
|
||||
print(f'- Median: {d[\"median_s\"]}s')
|
||||
else:
|
||||
print('- Benchmark script not present on base branch (skip comparison)')
|
||||
" >> "$GITHUB_STEP_SUMMARY"
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
|
||||
- name: Compare and gate
|
||||
run: |
|
||||
pr_median=${{ steps.pr.outputs.pr_median }}
|
||||
base_median=${{ steps.base.outputs.base_median }}
|
||||
|
||||
python3 -c "
|
||||
pr = float('$pr_median')
|
||||
base = float('$base_median')
|
||||
|
||||
if base <= 0:
|
||||
print('⏭️ No base benchmark available — skipping comparison.')
|
||||
exit(0)
|
||||
|
||||
change_pct = ((pr - base) / base) * 100
|
||||
print(f'Base: {base:.3f}s')
|
||||
print(f'PR: {pr:.3f}s')
|
||||
print(f'Change: {change_pct:+.1f}%')
|
||||
print()
|
||||
|
||||
if change_pct > 5:
|
||||
print(f'❌ BLOCKED: Import time regressed by {change_pct:.1f}% (threshold: 5%)')
|
||||
exit(1)
|
||||
elif change_pct > 0:
|
||||
print(f'⚠️ Slight regression ({change_pct:.1f}%) but within 5% threshold.')
|
||||
else:
|
||||
print(f'✅ Import time improved by {abs(change_pct):.1f}%')
|
||||
"
|
||||
Reference in New Issue
Block a user