diff --git a/static/js/progress.js b/static/js/progress.js
index a62569d..0fb9be8 100644
--- a/static/js/progress.js
+++ b/static/js/progress.js
@@ -29,6 +29,13 @@ function stopProgressPolling() {
}
function updateProgressUI(message, progress, total) {
+
+ if (window.progressInitTimeout) {
+ clearTimeout(window.progressInitTimeout);
+ window.progressInitTimeout = null;
+ }
+
+
const progressSection = document.getElementById('progressSection');
const progressBar = progressSection.querySelector('.progress-bar');
const progressMessage = document.getElementById('progressMessage');
@@ -66,6 +73,19 @@ function showProgress() {
progressSection.style.display = 'block';
document.getElementById('generateBtn').disabled = true;
+ window.progressInitTimeout = setTimeout(() => {
+ if (progressMessage && progressMessage.textContent === 'Initializing...') {
+ progressMessage.innerHTML = `
+
Initializing...
+
+ Taking longer than expected...
+ All workers may be busy processing other requests.
+ Please wait for the queue to clear.
+
+ `;
+ }
+ }, 5000);
+
startProgressPolling();
}
diff --git a/test_performance.py b/test_performance.py
index 93f1332..ca8babe 100644
--- a/test_performance.py
+++ b/test_performance.py
@@ -14,12 +14,14 @@ from collections import defaultdict
import sys
import argparse
import threading
+import json
class PerformanceTest:
def __init__(self, base_url):
self.base_url = base_url
self.results = defaultdict(list)
self.errors = []
+ self.error_samples = [] # Store sample responses
self.lock = threading.Lock()
self.available_countries = self._fetch_available_countries()
@@ -58,23 +60,107 @@ class PerformanceTest:
duration = time.time() - start
+ # Check status code
+ if resp.status_code != 200:
+ error_info = {
+ 'endpoint': endpoint,
+ 'error': f"HTTP {resp.status_code}",
+ 'duration': duration,
+ 'response_preview': resp.text[:300]
+ }
+
+ with self.lock:
+ self.errors.append(error_info)
+ if len(self.error_samples) < 3:
+ self.error_samples.append({
+ 'type': f'HTTP {resp.status_code}',
+ 'url': url,
+ 'status': resp.status_code,
+ 'headers': dict(resp.headers),
+ 'body': resp.text[:500]
+ })
+
+ return {
+ 'status': resp.status_code,
+ 'duration': duration,
+ 'size': 0,
+ 'success': False,
+ 'endpoint': endpoint,
+ 'error': f"HTTP {resp.status_code}"
+ }
+
+ # Check if this is a RAW endpoint (returns plaintext, not JSON)
+ is_raw_endpoint = '/api/generate/raw' in endpoint
+
+ if is_raw_endpoint:
+ # RAW endpoints return plaintext, not JSON
+ return {
+ 'status': resp.status_code,
+ 'duration': duration,
+ 'size': len(resp.content),
+ 'success': True,
+ 'endpoint': endpoint,
+ 'cache_type': None # RAW doesn't have cache_type in response
+ }
+
+ # Try to parse JSON for non-RAW endpoints
+ try:
+ json_data = resp.json()
+ except json.JSONDecodeError as e:
+ error_info = {
+ 'endpoint': endpoint,
+ 'error': f"Invalid JSON: {str(e)}",
+ 'duration': duration,
+ 'response_preview': resp.text[:300]
+ }
+
+ with self.lock:
+ self.errors.append(error_info)
+ if len(self.error_samples) < 3:
+ self.error_samples.append({
+ 'type': 'JSON Parse Error',
+ 'url': url,
+ 'status': resp.status_code,
+ 'headers': dict(resp.headers),
+ 'body': resp.text[:500],
+ 'error': str(e)
+ })
+
+ return {
+ 'status': resp.status_code,
+ 'duration': duration,
+ 'size': len(resp.content),
+ 'success': False,
+ 'endpoint': endpoint,
+ 'error': f"Invalid JSON: {str(e)}"
+ }
+
return {
'status': resp.status_code,
'duration': duration,
'size': len(resp.content),
- 'success': resp.status_code == 200,
+ 'success': True,
'endpoint': endpoint,
- 'cache_type': resp.json().get('cache_type') if resp.status_code == 200 else None
+ 'cache_type': json_data.get('cache_type')
}
except Exception as e:
duration = time.time() - start
+ error_info = {
+ 'endpoint': endpoint,
+ 'error': str(e),
+ 'duration': duration
+ }
+
with self.lock:
- self.errors.append({
- 'endpoint': endpoint,
- 'error': str(e),
- 'duration': duration
- })
+ self.errors.append(error_info)
+ if len(self.error_samples) < 3:
+ self.error_samples.append({
+ 'type': 'Exception',
+ 'url': url,
+ 'error': str(e)
+ })
+
return {
'status': 0,
'duration': duration,
@@ -83,6 +169,7 @@ class PerformanceTest:
'endpoint': endpoint,
'error': str(e)
}
+
def simulate_user(self, user_id, duration_seconds, think_time_range=(1, 5)):
"""Simulate single user behavior - ONLY single countries"""
@@ -272,9 +359,31 @@ class PerformanceTest:
for error, count in sorted(error_types.items(), key=lambda x: -x[1])[:10]:
self.log(f" {count:3d}x {error}")
+
+ # Show detailed error samples
+ if self.error_samples:
+ self.log(f"\n{'='*70}")
+ self.log("DETAILED ERROR SAMPLES")
+ self.log(f"{'='*70}")
+
+ for idx, sample in enumerate(self.error_samples, 1):
+ self.log(f"\nSample #{idx}: {sample['type']}")
+ self.log(f" URL: {sample['url']}")
+ if 'status' in sample:
+ self.log(f" Status: {sample['status']}")
+ if 'headers' in sample:
+ self.log(f" Content-Type: {sample['headers'].get('Content-Type', 'N/A')}")
+ self.log(f" Content-Length: {sample['headers'].get('Content-Length', 'N/A')}")
+ if 'error' in sample:
+ self.log(f" Error: {sample['error']}")
+ if 'body' in sample:
+ self.log(f" Response body preview:")
+ self.log(f" ---")
+ for line in sample['body'].split('\n')[:10]:
+ self.log(f" {line}")
+ self.log(f" ---")
-
-def run_user_simulation(base_url, num_users, duration):
+def run_user_simulation(base_url, num_users, duration, think_time):
"""Run only user simulation"""
tester = PerformanceTest(base_url)
@@ -282,13 +391,13 @@ def run_user_simulation(base_url, num_users, duration):
print("GEOIP BAN API - USER SIMULATION")
print(f"Target: {base_url}")
print(f"Simulating {num_users} concurrent users for {duration}s")
+ print(f"Think time: {think_time[0]}-{think_time[1]}s")
print("="*70 + "\n")
- tester.test_concurrent_users(num_users, duration, think_time_range=(1, 5))
+ tester.test_concurrent_users(num_users, duration, think_time_range=think_time)
tester.final_report()
-
-def run_quick_test(base_url):
+def run_quick_test(base_url, num_users, duration, think_time):
"""Quick performance test - single countries only"""
tester = PerformanceTest(base_url)
@@ -317,10 +426,87 @@ def run_quick_test(base_url):
count=50, concurrent=10
)
- tester.test_concurrent_users(num_users=10, duration_seconds=30, think_time_range=(1, 3))
+ tester.test_concurrent_users(num_users=num_users, duration_seconds=duration, think_time_range=think_time)
tester.final_report()
+def run_full_test_suite(base_url, num_users, duration, think_time):
+ """Execute complete test suite - single countries only"""
+ tester = PerformanceTest(base_url)
+
+ print("\n" + "="*70)
+ print("GEOIP BAN API - FULL PERFORMANCE TEST SUITE")
+ print(f"Target: {base_url}")
+ print("="*70 + "\n")
+
+ if not tester.available_countries:
+ print("ERROR: No countries available from API")
+ return
+
+ tester.test_scenario("Health Check", "GET", "/api/database/status", count=50, concurrent=10)
+ time.sleep(1)
+
+ test_country = tester.available_countries[0]
+
+ tester.test_scenario(
+ "Single Country (Cached)",
+ "POST", "/api/generate/preview",
+ data={'countries': [test_country], 'app_type': 'nginx', 'app_variant': 'geo', 'aggregate': True},
+ count=100, concurrent=20
+ )
+ time.sleep(1)
+
+ tester.test_scenario(
+ "Heavy Load (50 concurrent)",
+ "POST", "/api/generate/preview",
+ data={'countries': [test_country], 'app_type': 'nginx', 'app_variant': 'map', 'aggregate': True},
+ count=200, concurrent=50
+ )
+ time.sleep(2)
+
+ tester.log("\nSPIKE TEST - Sudden burst of 100 requests")
+ start = time.time()
+ tester.test_scenario(
+ "Spike Test",
+ "POST", "/api/generate/preview",
+ data={'countries': [test_country], 'app_type': 'apache', 'app_variant': '24', 'aggregate': True},
+ count=100, concurrent=100
+ )
+ spike_duration = time.time() - start
+ tester.log(f" Spike handled in: {spike_duration:.2f}s")
+ time.sleep(2)
+
+ tester.test_concurrent_users(num_users=num_users, duration_seconds=duration, think_time_range=think_time)
+ time.sleep(2)
+ tester.test_concurrent_users(num_users=num_users*5, duration_seconds=duration//2, think_time_range=(think_time[0]/2, think_time[1]/2))
+
+ tester.final_report()
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Performance testing for GeoIP Ban API')
+ parser.add_argument('--url', default='http://127.0.0.1:5000', help='API base URL')
+ parser.add_argument('--mode', choices=['quick', 'full', 'users'], default='quick',
+ help='Test mode: quick (5min), full (15min), users (simulation only)')
+ parser.add_argument('--users', type=int, default=10, help='Number of concurrent users')
+ parser.add_argument('--duration', type=int, default=60, help='Test duration in seconds')
+ parser.add_argument('--think-min', type=float, default=1.0, help='Minimum think time between requests (seconds)')
+ parser.add_argument('--think-max', type=float, default=5.0, help='Maximum think time between requests (seconds)')
+
+ args = parser.parse_args()
+
+ think_time = (args.think_min, args.think_max)
+
+ try:
+ if args.mode == 'quick':
+ run_quick_test(args.url, args.users, args.duration, think_time)
+ elif args.mode == 'full':
+ run_full_test_suite(args.url, args.users, args.duration, think_time)
+ elif args.mode == 'users':
+ run_user_simulation(args.url, args.users, args.duration, think_time)
+ except KeyboardInterrupt:
+ print("\n\nTest interrupted by user")
+ sys.exit(1)
+
def run_full_test_suite(base_url):
"""Execute complete test suite - single countries only"""
@@ -374,24 +560,27 @@ def run_full_test_suite(base_url):
tester.final_report()
-
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Performance testing for GeoIP Ban API')
parser.add_argument('--url', default='http://127.0.0.1:5000', help='API base URL')
parser.add_argument('--mode', choices=['quick', 'full', 'users'], default='quick',
help='Test mode: quick (5min), full (15min), users (simulation only)')
- parser.add_argument('--users', type=int, default=10, help='Number of concurrent users (for users mode)')
- parser.add_argument('--duration', type=int, default=60, help='Test duration in seconds (for users mode)')
+ parser.add_argument('--users', type=int, default=10, help='Number of concurrent users')
+ parser.add_argument('--duration', type=int, default=60, help='Test duration in seconds')
+ parser.add_argument('--think-min', type=float, default=1.0, help='Minimum think time between requests (seconds)')
+ parser.add_argument('--think-max', type=float, default=5.0, help='Maximum think time between requests (seconds)')
args = parser.parse_args()
+ think_time = (args.think_min, args.think_max)
+
try:
if args.mode == 'quick':
- run_quick_test(args.url)
+ run_quick_test(args.url, args.users, args.duration, think_time)
elif args.mode == 'full':
- run_full_test_suite(args.url)
+ run_full_test_suite(args.url, args.users, args.duration, think_time)
elif args.mode == 'users':
- run_user_simulation(args.url, args.users, args.duration)
+ run_user_simulation(args.url, args.users, args.duration, think_time)
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
sys.exit(1)