mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
417 lines
14 KiB
Python
417 lines
14 KiB
Python
"""Performance tests for MCP tools."""
|
|
|
|
import asyncio
|
|
import os
|
|
import tempfile
|
|
import time
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from tests.test_utils import create_mock_ctx
|
|
|
|
# Try to import memory tools, skip tests if not available
|
|
try:
|
|
from hanzo_tools.memory.memory_tools import (
|
|
CreateMemoriesTool,
|
|
RecallMemoriesTool,
|
|
)
|
|
|
|
MEMORY_TOOLS_AVAILABLE = True
|
|
except ImportError:
|
|
MEMORY_TOOLS_AVAILABLE = False
|
|
RecallMemoriesTool = None
|
|
CreateMemoriesTool = None
|
|
|
|
from hanzo_tools.agent.swarm_tool import SwarmTool
|
|
from hanzo_tools.filesystem.search_tool import SearchTool as UnifiedSearchTool
|
|
|
|
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
|
|
|
|
class TestMemoryPerformance:
|
|
"""Performance tests for memory tools."""
|
|
|
|
@pytest.mark.skipif(not MEMORY_TOOLS_AVAILABLE, reason="Memory tools not available")
|
|
@patch("hanzo_memory.services.memory.get_memory_service")
|
|
def test_bulk_memory_creation_performance(self, mock_get_service):
|
|
"""Test performance of bulk memory creation."""
|
|
mock_service = Mock()
|
|
created_count = 0
|
|
|
|
def mock_create(*args, **kwargs):
|
|
nonlocal created_count
|
|
created_count += 1
|
|
return Mock(memory_id=f"mem_{created_count}")
|
|
|
|
mock_service.create_memory = mock_create
|
|
mock_get_service.return_value = mock_service
|
|
|
|
tool = CreateMemoriesTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Create 1000 memories
|
|
memories = [f"Memory {i}" for i in range(1000)]
|
|
|
|
start_time = time.time()
|
|
result = asyncio.run(tool.call(mock_ctx, statements=memories))
|
|
elapsed = time.time() - start_time
|
|
|
|
assert "1000" in str(result) or created_count == 1000
|
|
assert created_count == 1000
|
|
assert elapsed < 5.0 # Should complete within 5 seconds
|
|
|
|
print(f"Created 1000 memories in {elapsed:.2f} seconds")
|
|
|
|
@pytest.mark.skip(reason="Test uses deprecated API - needs update")
|
|
def test_concurrent_memory_operations(self):
|
|
"""Test concurrent memory operations."""
|
|
mock_service = Mock()
|
|
operation_times = []
|
|
|
|
async def mock_operation(*args, **kwargs):
|
|
start = time.time()
|
|
await asyncio.sleep(0.01) # Simulate work
|
|
operation_times.append(time.time() - start)
|
|
return Mock(memory_id=f"mem_{len(operation_times)}")
|
|
|
|
mock_service.create_memory = Mock(side_effect=mock_operation)
|
|
mock_service.search_memories = Mock(
|
|
side_effect=lambda *a, **k: asyncio.create_task(mock_operation(*a, **k))
|
|
)
|
|
mock_get_service.return_value = mock_service
|
|
|
|
async def run_concurrent_operations():
|
|
"""Run multiple operations concurrently."""
|
|
create_tool = CreateMemoriesTool()
|
|
recall_tool = RecallMemoriesTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Run 10 operations concurrently
|
|
tasks = []
|
|
for i in range(5):
|
|
tasks.append(create_tool.call(mock_ctx, statements=[f"Memory {i}"]))
|
|
tasks.append(recall_tool.call(mock_ctx, queries=[f"Query {i}"]))
|
|
|
|
start_time = time.time()
|
|
await asyncio.gather(*tasks)
|
|
total_time = time.time() - start_time
|
|
|
|
return total_time, operation_times
|
|
|
|
total_time, op_times = asyncio.run(run_concurrent_operations())
|
|
|
|
# Should be faster than sequential execution
|
|
sequential_time = sum(op_times)
|
|
assert total_time < sequential_time * 0.5 # At least 2x speedup
|
|
|
|
print(
|
|
f"Concurrent: {total_time:.2f}s, Sequential would be: {sequential_time:.2f}s"
|
|
)
|
|
|
|
|
|
class TestSearchPerformance:
|
|
"""Performance tests for search tools."""
|
|
|
|
def test_large_directory_search(self, tool_helper):
|
|
"""Test search performance in large directory structure."""
|
|
# Create temporary directory with many files
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create 1000 files in nested structure
|
|
for i in range(10):
|
|
subdir = os.path.join(tmpdir, f"dir_{i}")
|
|
os.makedirs(subdir)
|
|
for j in range(100):
|
|
filepath = os.path.join(subdir, f"file_{j}.txt")
|
|
with open(filepath, "w") as f:
|
|
f.write(f"Content of file {i}_{j}\n")
|
|
if j % 10 == 0:
|
|
f.write("SPECIAL_PATTERN\n")
|
|
|
|
# Mock permission manager
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path(tmpdir)
|
|
|
|
# Create search tool
|
|
with patch("hanzo_tools.filesystem.search_tool.ProjectVectorManager"):
|
|
tool = UnifiedSearchTool(permission_manager=pm)
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Search for pattern
|
|
start_time = time.time()
|
|
result = asyncio.run(
|
|
tool.call(
|
|
mock_ctx, pattern="SPECIAL_PATTERN", path=tmpdir, max_results=50
|
|
)
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Should find matches quickly
|
|
tool_helper.assert_in_result("SPECIAL_PATTERN", result)
|
|
assert elapsed < 2.0 # Should complete within 2 seconds
|
|
|
|
print(f"Searched 1000 files in {elapsed:.2f} seconds")
|
|
|
|
|
|
class TestBatchToolPerformance:
|
|
"""Performance tests for batch tool."""
|
|
|
|
def test_large_batch_processing(self):
|
|
"""Test processing large batches of tool calls."""
|
|
# Create mock tools
|
|
mock_tools = {}
|
|
call_times = []
|
|
|
|
async def mock_tool_call(*args, **kwargs):
|
|
start = time.time()
|
|
await asyncio.sleep(0.01) # Simulate work
|
|
call_times.append(time.time() - start)
|
|
return f"Result {len(call_times)}"
|
|
|
|
for i in range(10):
|
|
tool = Mock()
|
|
tool.name = f"tool_{i}"
|
|
tool.call = Mock(side_effect=mock_tool_call)
|
|
mock_tools[tool.name] = tool
|
|
|
|
batch_tool = BatchTool(mock_tools)
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Create 50 invocations
|
|
invocations = []
|
|
for i in range(50):
|
|
invocations.append({"tool_name": f"tool_{i % 10}", "input": {"param": i}})
|
|
|
|
# Process batch
|
|
start_time = time.time()
|
|
result = asyncio.run(
|
|
batch_tool.call(
|
|
mock_ctx, description="Large batch test", invocations=invocations
|
|
)
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Should process efficiently
|
|
assert "error" not in result.lower()
|
|
|
|
# Should be much faster than sequential
|
|
sequential_time = len(invocations) * 0.01
|
|
assert elapsed < sequential_time * 0.5 # At least 2x speedup
|
|
|
|
print(f"Processed 50 tool calls in {elapsed:.2f} seconds")
|
|
|
|
|
|
class TestSwarmPerformance:
|
|
"""Performance tests for swarm tool."""
|
|
|
|
@pytest.mark.skip(
|
|
reason="Test uses deprecated dispatch_to_model API - swarm now uses hanzo-agents SDK"
|
|
)
|
|
def test_parallel_agent_execution(self):
|
|
"""Test parallel execution of swarm agents."""
|
|
execution_times = []
|
|
|
|
async def mock_agent_execution(*args, **kwargs):
|
|
start = time.time()
|
|
await asyncio.sleep(0.1) # Simulate agent work
|
|
execution_times.append(time.time() - start)
|
|
return f"Agent result {len(execution_times)}"
|
|
|
|
mock_dispatch.side_effect = mock_agent_execution
|
|
|
|
tool = SwarmTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Create parallel agent network
|
|
agents = []
|
|
for i in range(10):
|
|
agents.append({"id": f"agent_{i}", "query": f"Task {i}", "role": "worker"})
|
|
|
|
# Add a final reviewer that depends on all
|
|
agents.append(
|
|
{
|
|
"id": "reviewer",
|
|
"query": "Review all results",
|
|
"role": "reviewer",
|
|
"receives_from": [f"agent_{i}" for i in range(10)],
|
|
}
|
|
)
|
|
|
|
# Execute swarm
|
|
start_time = time.time()
|
|
asyncio.run(
|
|
tool.call(mock_ctx, query="Parallel processing test", agents=agents)
|
|
)
|
|
elapsed = time.time() - start_time
|
|
|
|
# Should execute workers in parallel
|
|
# Sequential would be 11 * 0.1 = 1.1 seconds
|
|
# Parallel should be ~0.2 seconds (2 phases)
|
|
assert elapsed < 0.6 # Allow some overhead
|
|
|
|
print(f"Executed {len(agents)} agents in {elapsed:.2f} seconds")
|
|
|
|
|
|
class TestPaginationPerformance:
|
|
"""Test pagination system performance."""
|
|
|
|
def test_large_output_pagination(self):
|
|
"""Test pagination with very large outputs."""
|
|
from hanzo_mcp.tools.common.paginated_response import AutoPaginatedResponse
|
|
from hanzo_mcp.tools.common.truncate import estimate_tokens
|
|
|
|
# Create large dataset
|
|
large_data = []
|
|
for i in range(10000):
|
|
large_data.append(
|
|
{
|
|
"id": i,
|
|
"content": f"This is item {i} with some content that makes it larger",
|
|
"metadata": {"category": i % 10, "priority": i % 5},
|
|
}
|
|
)
|
|
|
|
# Test token estimation performance
|
|
start_time = time.time()
|
|
total_tokens = 0
|
|
for item in large_data[:1000]: # Test first 1000
|
|
total_tokens += estimate_tokens(str(item))
|
|
estimation_time = time.time() - start_time
|
|
|
|
assert estimation_time < 1.0 # Should be fast
|
|
print(f"Estimated tokens for 1000 items in {estimation_time:.2f} seconds")
|
|
|
|
# Test pagination creation with new API
|
|
start_time = time.time()
|
|
|
|
# Use the new AutoPaginatedResponse API
|
|
content_str = "\n".join([str(item) for item in large_data])
|
|
response = AutoPaginatedResponse.create_response(content_str)
|
|
|
|
pagination_time = time.time() - start_time
|
|
|
|
assert "content" in response # New API returns content dict
|
|
assert pagination_time < 0.5 # Should be very fast
|
|
|
|
print(f"Created paginated response in {pagination_time:.2f} seconds")
|
|
|
|
|
|
class TestMemoryStressTest:
|
|
"""Stress tests for memory system."""
|
|
|
|
@pytest.mark.skip(reason="Test uses deprecated API - needs update")
|
|
def test_memory_system_under_load(self):
|
|
"""Test memory system under heavy load."""
|
|
mock_service = Mock()
|
|
|
|
# Track all operations
|
|
operations = []
|
|
|
|
def track_operation(op_type):
|
|
def wrapper(*args, **kwargs):
|
|
operations.append((op_type, time.time()))
|
|
return Mock(memory_id=f"mem_{len(operations)}")
|
|
|
|
return wrapper
|
|
|
|
mock_service.create_memory = track_operation("create")
|
|
mock_service.search_memories = Mock(side_effect=lambda *a, **k: [])
|
|
mock_service.delete_memory = Mock(side_effect=lambda *a, **k: True)
|
|
mock_get_service.return_value = mock_service
|
|
|
|
async def stress_test():
|
|
"""Run many operations concurrently."""
|
|
create_tool = CreateMemoriesTool()
|
|
recall_tool = RecallMemoriesTool()
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
tasks = []
|
|
|
|
# Create 100 concurrent operations
|
|
for i in range(100):
|
|
if i % 3 == 0:
|
|
tasks.append(create_tool.call(mock_ctx, statements=[f"Memory {i}"]))
|
|
else:
|
|
tasks.append(recall_tool.call(mock_ctx, queries=[f"Query {i}"]))
|
|
|
|
start_time = time.time()
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
return time.time() - start_time
|
|
|
|
elapsed = asyncio.run(stress_test())
|
|
|
|
# Should handle load without crashing
|
|
assert len(operations) > 30 # At least the creates
|
|
assert elapsed < 5.0 # Should complete reasonably fast
|
|
|
|
print(
|
|
f"Completed {len(operations)} operations under load in {elapsed:.2f} seconds"
|
|
)
|
|
|
|
|
|
class TestConcurrentFileOperations:
|
|
"""Test concurrent file operations."""
|
|
|
|
def test_concurrent_file_access(self):
|
|
"""Test multiple tools accessing files concurrently."""
|
|
from hanzo_tools.filesystem.read import ReadTool
|
|
from hanzo_tools.filesystem.write import Write as WriteTool
|
|
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path(tmpdir)
|
|
|
|
# Create test files
|
|
test_files = []
|
|
for i in range(20):
|
|
filepath = os.path.join(tmpdir, f"test_{i}.txt")
|
|
with open(filepath, "w") as f:
|
|
f.write(f"Initial content {i}\n" * 100)
|
|
test_files.append(filepath)
|
|
|
|
read_tool = ReadTool(pm)
|
|
write_tool = WriteTool(pm)
|
|
|
|
async def concurrent_operations():
|
|
"""Run concurrent read/write operations."""
|
|
tasks = []
|
|
mock_ctx = create_mock_ctx()
|
|
|
|
# Mix of reads and writes
|
|
for i, filepath in enumerate(test_files):
|
|
if i % 3 == 0:
|
|
# Write operation
|
|
tasks.append(
|
|
write_tool.call(
|
|
mock_ctx,
|
|
file_path=filepath,
|
|
content=f"Updated content {i}\n" * 100,
|
|
)
|
|
)
|
|
else:
|
|
# Read operation
|
|
tasks.append(read_tool.call(mock_ctx, file_path=filepath))
|
|
|
|
start_time = time.time()
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
return time.time() - start_time, results
|
|
|
|
elapsed, results = asyncio.run(concurrent_operations())
|
|
|
|
# Should complete without errors
|
|
errors = [r for r in results if isinstance(r, Exception)]
|
|
assert len(errors) == 0
|
|
assert elapsed < 2.0 # Should be fast even with 20 operations
|
|
|
|
print(
|
|
f"Completed {len(results)} concurrent file operations in {elapsed:.2f} seconds"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "-s"]) # -s to see print statements
|