Skip to content

Commit 30e2b76

Browse files
committed
[minimcp] Add comprehensive benchmark suite and performance analysis
- Add benchmarking framework and infrastructure - Add benchmarks for all three transports (stdio, HTTP, streamable HTTP) - Add analysis tools and comprehensive performance report - Include benchmark results for sync and async tool calls
1 parent 872d6f3 commit 30e2b76

25 files changed

+5426
-1
lines changed

benchmarks/minimcp/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# MiniMCP vs FastMCP · Benchmarks
2+
3+
Latest report: [Comprehensive Benchmark Report](./reports/COMPREHENSIVE_BENCHMARK_REPORT.md)
4+
5+
Once you've set up a development environment as described in [CONTRIBUTING.md](../../CONTRIBUTING.md), you can run the benchmark scripts.
6+
7+
## Running Benchmarks
8+
9+
Each transport has a separate benchmark script that can be run with the following commands. Only tool calling is used for benchmarking as other primitives aren't much different functionally. Each script produces two result files: one for sync tool calls and another for async tool calls.
10+
11+
```bash
12+
# Stdio
13+
uv run python -m benchmarks.minimcp.macro.stdio_mcp_server_benchmark
14+
15+
# HTTP
16+
uv run python -m benchmarks.minimcp.macro.http_mcp_server_benchmark
17+
18+
# Streamable HTTP
19+
uv run python -m benchmarks.minimcp.macro.streamable_http_mcp_server_benchmark
20+
```
21+
22+
### System Preparation - Best practice in Ubuntu
23+
24+
The following steps can help you get consistent benchmark results. They are specifically for Ubuntu, but similar steps may exist for other operating systems.
25+
26+
```bash
27+
# Stop unnecessary services
28+
sudo systemctl stop snapd
29+
sudo systemctl stop unattended-upgrades
30+
31+
# Disable CPU frequency scaling (use performance governor)
32+
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
33+
34+
# Disable turbo boost for consistency (optional but recommended)
35+
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # Intel
36+
# OR for AMD:
37+
echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost
38+
```
39+
40+
After the above steps, you can run the benchmark scripts with `taskset` to pin to specific CPU cores. This ensures the benchmark always runs on the same CPU cores, avoiding cache misses and CPU migration overhead.
41+
42+
```bash
43+
taskset -c 0-3 uv run python -m <benchmark.module>
44+
```
45+
46+
### Load Profiles
47+
48+
The benchmark uses four load profiles to test performance under different concurrency levels:
49+
50+
| Load | Concurrency | Iterations | Rounds | Total Messages |
51+
|------------|-------------|------------|--------|----------------|
52+
| Sequential | 1 | 30 | 40 | 1,200 |
53+
| Light | 20 | 30 | 40 | 24,000 |
54+
| Medium | 100 | 15 | 40 | 60,000 |
55+
| Heavy | 300 | 15 | 40 | 180,000 |
56+
57+
## Analyze Results
58+
59+
The `analyze_results.py` script provides a visual comparison of benchmark results between MiniMCP and FastMCP. It displays response time comparisons across all load profiles with visual bar charts, performance improvements as percentages, memory usage comparisons, key findings, and metadata.
60+
61+
You can run it for each result JSON file with:
62+
63+
```bash
64+
# Stdio
65+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/stdio_mcp_server_sync_benchmark_results.json
66+
67+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/stdio_mcp_server_async_benchmark_results.json
68+
69+
# HTTP
70+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/http_mcp_server_sync_benchmark_results.json
71+
72+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/http_mcp_server_async_benchmark_results.json
73+
74+
# Streamable HTTP
75+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/streamable_http_mcp_server_sync_benchmark_results.json
76+
77+
uv run python benchmarks/minimcp/analyze_results.py benchmarks/minimcp/reports/streamable_http_mcp_server_async_benchmark_results.json
78+
```
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""
2+
Analyze and visualize MCP server benchmark results.
3+
Analyzer generated by Claude 4.5 Sonnet.
4+
"""
5+
6+
import json
7+
import sys
8+
from pathlib import Path
9+
from typing import Any
10+
11+
LOAD_INFO = {
12+
"sequential_load": ("Sequential Load", "1 concurrent request"),
13+
"light_load": ("Light Load", "20 concurrent requests"),
14+
"medium_load": ("Medium Load", "100 concurrent requests"),
15+
"heavy_load": ("Heavy Load", "300 concurrent requests"),
16+
}
17+
18+
19+
def load_results(json_path: Path) -> dict[str, Any]:
20+
"""Load benchmark results from JSON file."""
21+
22+
if not json_path.exists():
23+
print(f"Error: File not found: {json_path}")
24+
sys.exit(1)
25+
26+
with open(json_path) as f:
27+
return json.load(f)
28+
29+
30+
def calculate_improvement(minimcp_val: float, fastmcp_val: float, lower_is_better: bool = True) -> float:
31+
"""Calculate percentage improvement."""
32+
if lower_is_better:
33+
return ((fastmcp_val - minimcp_val) / fastmcp_val) * 100
34+
else:
35+
return ((minimcp_val - fastmcp_val) / fastmcp_val) * 100
36+
37+
38+
def print_title(title: str) -> None:
39+
# Bold + Underline
40+
print("\033[1m\033[4m" + title + "\033[0m\n")
41+
42+
43+
def organize_results(results: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
44+
"""Organize results by server and load."""
45+
data: dict[str, dict[str, Any]] = {}
46+
for result in results["results"]:
47+
server = result["server_name"]
48+
load = result["load_name"]
49+
if server not in data:
50+
data[server] = {}
51+
data[server][load] = result["metrics"]
52+
53+
return data["minimcp"], data["fastmcp"]
54+
55+
56+
def print_metadata(results: dict[str, Any]) -> None:
57+
"""Print metadata."""
58+
min, sec = divmod(results["metadata"]["duration_seconds"], 60)
59+
print(f"Date: {results['metadata']['timestamp']}")
60+
print(f"Duration: {min:.0f}m {sec:.0f}s\n")
61+
62+
63+
def print_key_findings(results: dict[str, Any]) -> None:
64+
"""Print key findings section."""
65+
print_title("Key Findings")
66+
67+
minimcp, fastmcp = organize_results(results)
68+
69+
# Response time improvements (excluding sequential)
70+
response_improvements: list[float] = []
71+
for load in ["light_load", "medium_load", "heavy_load"]:
72+
min_rt = minimcp[load]["response_time"]["mean"]
73+
fast_rt = fastmcp[load]["response_time"]["mean"]
74+
improvement = calculate_improvement(min_rt, fast_rt, lower_is_better=True)
75+
response_improvements.append(improvement)
76+
77+
rt_min = min(response_improvements)
78+
rt_max = max(response_improvements)
79+
80+
# Throughput improvements
81+
throughput_improvements: list[float] = []
82+
for load in ["light_load", "medium_load", "heavy_load"]:
83+
min_tp = minimcp[load]["throughput_rps"]["mean"]
84+
fast_tp = fastmcp[load]["throughput_rps"]["mean"]
85+
improvement = calculate_improvement(min_tp, fast_tp, lower_is_better=False)
86+
throughput_improvements.append(improvement)
87+
88+
tp_min = min(throughput_improvements)
89+
tp_max = max(throughput_improvements)
90+
91+
# Memory improvements
92+
memory_improvements: list[float] = []
93+
for load in ["medium_load", "heavy_load"]:
94+
min_mem = minimcp[load]["max_memory_usage"]["mean"]
95+
fast_mem = fastmcp[load]["max_memory_usage"]["mean"]
96+
improvement = calculate_improvement(min_mem, fast_mem, lower_is_better=True)
97+
memory_improvements.append(improvement)
98+
99+
mem_min = min(memory_improvements)
100+
mem_max = max(memory_improvements)
101+
102+
print(
103+
f"- MiniMCP outperforms FastMCP by ~{rt_min:.0f}-{rt_max:.0f}% in response time across "
104+
"all concurrent load scenarios"
105+
)
106+
print(f"- MiniMCP achieves ~{tp_min:.0f}-{tp_max:.0f}% higher throughput than FastMCP")
107+
108+
# Handle memory improvements (can be positive or negative)
109+
if mem_min >= 0 and mem_max >= 0:
110+
print(f"- MiniMCP uses ~{mem_min:.0f}-{mem_max:.0f}% less memory under medium to heavy loads")
111+
elif mem_min < 0 and mem_max < 0:
112+
print(f"- MiniMCP uses ~{abs(mem_max):.0f}-{abs(mem_min):.0f}% more memory under medium to heavy loads")
113+
else:
114+
print(
115+
f"- MiniMCP memory usage varies from {mem_min:.0f}% to {mem_max:.0f}% compared to FastMCP under medium "
116+
"to heavy loads"
117+
)
118+
print()
119+
120+
121+
def print_response_time_visualization(results: dict[str, Any]) -> None:
122+
"""Print response time visualization."""
123+
print_title("Response Time Visualization (smaller is better)")
124+
125+
minimcp, fastmcp = organize_results(results)
126+
127+
for load_key, (title, subtitle) in LOAD_INFO.items():
128+
min_rt = minimcp[load_key]["response_time"]["mean"] * 1000 # to ms
129+
fast_rt = fastmcp[load_key]["response_time"]["mean"] * 1000
130+
improvement = calculate_improvement(min_rt, fast_rt, lower_is_better=True)
131+
132+
# Scale bars (max 50 chars for fastmcp)
133+
max_val = max(min_rt, fast_rt)
134+
fast_bars = int((fast_rt / max_val) * 50)
135+
min_bars = int((min_rt / max_val) * 50)
136+
137+
# Determine if minimcp is better or worse
138+
if improvement > 0:
139+
status = f"✓ {improvement:.1f}% faster"
140+
else:
141+
status = f"✗ {abs(improvement):.1f}% slower"
142+
143+
print(f"{title} ({subtitle})")
144+
print(f"minimcp {'▓' * min_bars} {min_rt:.2f}ms {status}")
145+
print(f"fastmcp {'▓' * fast_bars} {fast_rt:.2f}ms")
146+
print()
147+
print()
148+
149+
150+
def print_memory_visualization(results: dict[str, Any]) -> None:
151+
"""Print maximum memory usage visualization."""
152+
print_title("Maximum Memory Usage Visualization (smaller is better)")
153+
154+
minimcp, fastmcp = organize_results(results)
155+
156+
for load_key, (title, subtitle) in LOAD_INFO.items():
157+
min_mem = minimcp[load_key]["max_memory_usage"]["mean"]
158+
fast_mem = fastmcp[load_key]["max_memory_usage"]["mean"]
159+
improvement = calculate_improvement(min_mem, fast_mem, lower_is_better=True)
160+
161+
# Scale bars (max 50 chars for the higher value)
162+
max_val = max(min_mem, fast_mem)
163+
min_bars = int((min_mem / max_val) * 50)
164+
fast_bars = int((fast_mem / max_val) * 50)
165+
166+
# Determine if minimcp is better or worse
167+
if improvement > 0:
168+
status = f"✓ {improvement:.1f}% lower"
169+
else:
170+
status = f"✗ {abs(improvement):.1f}% higher"
171+
172+
print(f"{title} ({subtitle})")
173+
print(f"minimcp {'▓' * min_bars} {min_mem:,.0f} KB {status}")
174+
print(f"fastmcp {'▓' * fast_bars} {fast_mem:,.0f} KB")
175+
print()
176+
print()
177+
178+
179+
def main() -> None:
180+
"""Main entry point."""
181+
if len(sys.argv) != 2:
182+
print("Usage: python analyze_results.py <results.json>")
183+
sys.exit(1)
184+
185+
json_path = Path(sys.argv[1])
186+
results = load_results(json_path)
187+
188+
print()
189+
print_title("Benchmark Analysis")
190+
191+
print_metadata(results)
192+
print_key_findings(results)
193+
print_response_time_visualization(results)
194+
print_memory_visualization(results)
195+
196+
197+
if __name__ == "__main__":
198+
main()

benchmarks/minimcp/configs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
3+
from benchmarks.minimcp.core.mcp_server_benchmark import Load
4+
5+
# --- Server Configuration ---
6+
7+
SERVER_HOST = os.environ.get("TEST_SERVER_HOST", "127.0.0.1")
8+
SERVER_PORT = int(os.environ.get("TEST_SERVER_PORT", "30789"))
9+
10+
HTTP_MCP_PATH = "/mcp"
11+
12+
13+
# --- Paths ---
14+
15+
REPORTS_DIR = "benchmarks/minimcp/reports"
16+
17+
# --- Load Configuration ---
18+
19+
LOADS = [
20+
Load(name="sequential_load", concurrency=1, iterations=30, rounds=40),
21+
Load(name="light_load", concurrency=20, iterations=30, rounds=40),
22+
Load(name="medium_load", concurrency=100, iterations=15, rounds=40),
23+
Load(name="heavy_load", concurrency=300, iterations=15, rounds=40),
24+
]

0 commit comments

Comments
 (0)