From 0022d1ad629223bf65546e92dbf77d54a64eddc5 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 27 May 2026 12:53:42 +0800 Subject: [PATCH] test: add benchmark script for GET /projects performance testing --- tests/stress/benchmark_running_server.py | 288 +++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tests/stress/benchmark_running_server.py diff --git a/tests/stress/benchmark_running_server.py b/tests/stress/benchmark_running_server.py new file mode 100644 index 000000000..dd6774ef7 --- /dev/null +++ b/tests/stress/benchmark_running_server.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +""" +Benchmark test for GET /projects performance on a running GNS3 server + +This test connects to an already running GNS3 server and measures +the response time of the GET /projects endpoint. +""" + +import asyncio +import time +import argparse +import sys +import uuid +from pathlib import Path + +import httpx + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + + +async def create_test_projects(base_url, headers, count): + """Create specified number of test projects""" + print(f"Creating {count} test projects...") + + async with httpx.AsyncClient() as client: + batch_size = 50 + + for batch_start in range(0, count, batch_size): + batch_end = min(batch_start + batch_size, count) + batch_tasks = [] + + for i in range(batch_start, batch_end): + name = f"perf_test_project_{i}_{uuid.uuid4().hex[:8]}" + task = client.post( + f"{base_url}/projects", + headers=headers, + json={"name": name} + ) + batch_tasks.append(task) + + results = await asyncio.gather(*batch_tasks, return_exceptions=True) + + for i, result in enumerate(results, batch_start): + if hasattr(result, 'status_code'): + if result.status_code == 201: + print(f" Created project {i+1}/{count}") + else: + print(f" Failed to create project {i+1}: {result.status_code}") + else: + print(f" Error creating project {i+1}: {result}") + + +async def cleanup_test_projects(base_url, headers): + """Clean up test projects""" + print("Cleaning up test projects...") + + try: + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.get(f"{base_url}/projects", headers=headers) + if response.status_code == 200: + projects = response.json() + + test_projects = [p for p in projects if p.get('name', '').startswith('perf_test_project_')] + print(f" Found {len(test_projects)} test projects to delete") + + batch_size = 50 + for batch_start in range(0, len(test_projects), batch_size): + batch_end = min(batch_start + batch_size, len(test_projects)) + batch_tasks = [] + + for project in test_projects[batch_start:batch_end]: + task = client.delete( + f"{base_url}/projects/{project['project_id']}", + headers=headers + ) + batch_tasks.append(task) + + results = await asyncio.gather(*batch_tasks, return_exceptions=True) + + for i, result in enumerate(results, batch_start): + project = test_projects[i] + if hasattr(result, 'status_code'): + if result.status_code == 204: + print(f" Deleted {project['name']}") + else: + print(f" Failed to delete {project['name']}: {result.status_code}") + else: + print(f" Error deleting {project['name']}: {result}") + + except Exception as e: + print(f"Error during cleanup: {type(e).__name__}: {e}") + + +async def benchmark_get_projects(base_url, headers, project_count, iterations=10): + """Benchmark GET /projects endpoint""" + + print(f"\n{'='*60}") + print(f"GET /projects Performance Benchmark") + print(f"{'='*60}") + print(f"Server: {base_url}") + print(f"{'='*60}\n") + + if project_count > 0: + await create_test_projects(base_url, headers, project_count) + + # Warm-up run + print("Performing warm-up run...") + try: + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.get(f"{base_url}/projects", headers=headers) + print(f" Response status: {response.status_code}") + if response.status_code != 200: + print(f"Error during warm-up: HTTP {response.status_code}") + print(f"Response text: {response.text[:200]}") + return + actual_count = len(response.json()) + print(f" Warm-up successful: {actual_count} projects") + except Exception as e: + print(f"Error during warm-up: {type(e).__name__}: {e}") + return + + # Benchmark runs + response_times = [] + project_counts = [] + + print(f"\nRunning {iterations} benchmark iterations...") + async with httpx.AsyncClient(timeout=120.0) as client: + for i in range(iterations): + start_time = time.time() + try: + response = await client.get(f"{base_url}/projects", headers=headers) + end_time = time.time() + + if response.status_code == 200: + elapsed_ms = (end_time - start_time) * 1000 + projects = response.json() + response_times.append(elapsed_ms) + project_counts.append(len(projects)) + print(f" Iteration {i+1}: {elapsed_ms:.2f}ms ({len(projects)} projects)") + else: + print(f" Iteration {i+1}: ERROR {response.status_code}") + except Exception as e: + print(f" Iteration {i+1}: Exception - {type(e).__name__}: {str(e)}") + + # Cleanup test projects + if project_count > 0: + await cleanup_test_projects(base_url, headers) + + # Calculate statistics + if response_times: + avg_time = sum(response_times) / len(response_times) + min_time = min(response_times) + max_time = max(response_times) + median_time = sorted(response_times)[len(response_times) // 2] + avg_project_count = sum(project_counts) / len(project_counts) if project_counts else 0 + + print(f"\n{'='*60}") + if project_count > 0: + print(f"Results for {project_count} projects (created specifically):") + else: + print(f"Results for existing projects (~{avg_project_count:.0f} per request):") + print(f"{'='*60}") + print(f"Average: {avg_time:.2f}ms") + print(f"Median: {median_time:.2f}ms") + print(f"Min: {min_time:.2f}ms") + print(f"Max: {max_time:.2f}ms") + print(f"{'='*60}") + + # Performance assessment + if avg_time < 100: + status = "EXCELLENT" + elif avg_time < 500: + status = "ACCEPTABLE" + elif avg_time < 2000: + status = "SLOW" + else: + status = "CRITICAL" + + print(f"Status: {status}") + + # Throughput: projects returned per second + if project_counts and sum(response_times) > 0: + total_projects = sum(project_counts) + total_seconds = sum(response_times) / 1000 + projects_per_second = total_projects / total_seconds + print(f"Throughput: {projects_per_second:.1f} projects/sec") + print(f"{'='*60}\n") + + return { + 'project_count': project_count, + 'avg_time_ms': avg_time, + 'median_time_ms': median_time, + 'min_time_ms': min_time, + 'max_time_ms': max_time, + 'projects_per_second': projects_per_second if project_counts and sum(response_times) > 0 else 0 + } + + +async def main(): + parser = argparse.ArgumentParser(description='Benchmark GET /projects performance on running server') + parser.add_argument('--host', type=str, default='127.0.0.1', help='GNS3 server host') + parser.add_argument('--port', type=int, default=3080, help='GNS3 server port') + parser.add_argument('--username', type=str, required=True, help='Username for authentication') + parser.add_argument('--password', type=str, required=True, help='Password for authentication') + parser.add_argument('--project-counts', type=int, nargs='+', default=[10, 50, 100], + help='Numbers of projects to test (default: 10 50 100)') + parser.add_argument('--iterations', type=int, default=10, + help='Number of iterations per test (default: 10)') + parser.add_argument('--full-scale', action='store_true', + help='Run full scale test: 10, 50, 100, 200, 500 projects') + parser.add_argument('--no-cleanup', action='store_true', + help='Do not create/cleanup test projects (use existing projects)') + + args = parser.parse_args() + + base_url = f"http://{args.host}:{args.port}/v3" + + # Login to get JWT token + print(f"Connecting to {base_url}") + print(f"Authenticating as {args.username}...") + + try: + async with httpx.AsyncClient() as client: + login_response = await client.post( + f"{base_url}/access/users/login", + data={"username": args.username, "password": args.password} + ) + + if login_response.status_code != 200: + print(f"Login failed: {login_response.status_code}") + print(f"Response: {login_response.text}") + sys.exit(1) + + token_data = login_response.json() + access_token = token_data.get("access_token") + headers = {"Authorization": f"Bearer {access_token}"} + print("Authentication successful") + + except Exception as e: + print(f"Error connecting to server: {e}") + print("Make sure GNS3 server is running and accessible") + sys.exit(1) + + # Determine test scope + if args.full_scale: + project_counts = [10, 50, 100, 200, 500] + else: + project_counts = args.project_counts + + # If no-cleanup flag, test with existing projects as-is + if args.no_cleanup: + project_counts = [0] + args.iterations = 20 + + results = [] + + for count in project_counts: + try: + result = await benchmark_get_projects(base_url, headers, count, args.iterations) + if result: + results.append(result) + except Exception as e: + print(f"Error testing {count} projects: {e}") + + # Summary + if len(results) > 1: + print(f"\n{'='*60}") + print("Summary - Performance Scaling") + print(f"{'='*60}") + print(f"{'Projects':<12} {'Avg (ms)':<12} {'Status':<12}") + print(f"{'-'*40}") + for r in results: + avg = r['avg_time_ms'] + if avg < 100: + status = "EXCELLENT" + elif avg < 500: + status = "ACCEPTABLE" + elif avg < 2000: + status = "SLOW" + else: + status = "CRITICAL" + print(f"{r['project_count']:<12} {avg:<12.2f} {status:<12}") + print(f"{'='*60}\n") + + +if __name__ == "__main__": + asyncio.run(main())