149 lines
5.3 KiB
Python
149 lines
5.3 KiB
Python
|
|
import asyncio
|
||
|
|
import time
|
||
|
|
import random
|
||
|
|
import statistics
|
||
|
|
from collections import deque
|
||
|
|
import websockets
|
||
|
|
|
||
|
|
# --- Test Configuration ---
|
||
|
|
HOST = "127.0.0.1"
|
||
|
|
PORT = 8080
|
||
|
|
URI = f"ws://{HOST}:{PORT}"
|
||
|
|
|
||
|
|
# Client setup
|
||
|
|
NUM_SUBSCRIBERS = 1000
|
||
|
|
NUM_PUBLISHERS = 10
|
||
|
|
CHANNELS = ["news", "sports", "tech", "finance", "weather"]
|
||
|
|
|
||
|
|
# Test execution
|
||
|
|
TEST_DURATION_S = 15
|
||
|
|
MESSAGES_PER_SECOND_PER_PUBLISHER = 100 # Increased message rate
|
||
|
|
|
||
|
|
# --- Global State & Metrics ---
|
||
|
|
latencies = deque()
|
||
|
|
messages_sent = 0
|
||
|
|
messages_received = 0
|
||
|
|
subscriber_setup_count = 0
|
||
|
|
all_subscribed_event = asyncio.Event()
|
||
|
|
|
||
|
|
async def subscriber_client(client_id: int):
|
||
|
|
global subscriber_setup_count, messages_received
|
||
|
|
channel = random.choice(CHANNELS)
|
||
|
|
|
||
|
|
try:
|
||
|
|
async with websockets.connect(URI) as websocket:
|
||
|
|
await websocket.send(f"sub {channel}")
|
||
|
|
subscriber_setup_count += 1
|
||
|
|
if subscriber_setup_count == NUM_SUBSCRIBERS:
|
||
|
|
print("✅ All subscribers are connected and subscribed. Starting publishers...")
|
||
|
|
all_subscribed_event.set()
|
||
|
|
|
||
|
|
while True:
|
||
|
|
message = await websocket.recv()
|
||
|
|
try:
|
||
|
|
sent_time_str = message.split(":", 1)[0]
|
||
|
|
sent_time = float(sent_time_str)
|
||
|
|
latency = time.time() - sent_time
|
||
|
|
latencies.append(latency)
|
||
|
|
messages_received += 1
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
print(f"Warning: Received malformed message: {message}")
|
||
|
|
|
||
|
|
except (websockets.exceptions.ConnectionClosedError, ConnectionRefusedError) as e:
|
||
|
|
print(f"Subscriber {client_id} disconnected: {e}")
|
||
|
|
except asyncio.CancelledError:
|
||
|
|
pass
|
||
|
|
except Exception as e:
|
||
|
|
print(f"An unexpected error occurred in subscriber {client_id}: {e}")
|
||
|
|
|
||
|
|
async def publisher_client(client_id: int):
|
||
|
|
global messages_sent
|
||
|
|
await all_subscribed_event.wait()
|
||
|
|
|
||
|
|
sleep_interval = 1.0 / MESSAGES_PER_SECOND_PER_PUBLISHER
|
||
|
|
|
||
|
|
try:
|
||
|
|
async with websockets.connect(URI) as websocket:
|
||
|
|
while True:
|
||
|
|
channel = random.choice(CHANNELS)
|
||
|
|
send_time = time.time()
|
||
|
|
message = f"{send_time:.6f}:Hello from publisher {client_id} on channel {channel}"
|
||
|
|
|
||
|
|
await websocket.send(f"pub {channel} {message}")
|
||
|
|
messages_sent += 1
|
||
|
|
|
||
|
|
await asyncio.sleep(sleep_interval)
|
||
|
|
|
||
|
|
except (websockets.exceptions.ConnectionClosedError, ConnectionRefusedError) as e:
|
||
|
|
print(f"Publisher {client_id} disconnected: {e}")
|
||
|
|
except asyncio.CancelledError:
|
||
|
|
pass
|
||
|
|
except Exception as e:
|
||
|
|
print(f"An unexpected error occurred in publisher {client_id}: {e}")
|
||
|
|
|
||
|
|
def print_report():
|
||
|
|
print("\n" + "="*80)
|
||
|
|
print("PERFORMANCE REPORT".center(80))
|
||
|
|
print("="*80)
|
||
|
|
|
||
|
|
if not latencies:
|
||
|
|
print("No messages were received. Cannot generate a report. Is the server running?")
|
||
|
|
return
|
||
|
|
|
||
|
|
total_sent = messages_sent
|
||
|
|
total_received = messages_received
|
||
|
|
message_loss = max(0, total_sent - total_received)
|
||
|
|
loss_rate = (message_loss / total_sent * 100) if total_sent > 0 else 0
|
||
|
|
throughput = total_received / TEST_DURATION_S
|
||
|
|
|
||
|
|
print(f"Test Duration: {TEST_DURATION_S} seconds")
|
||
|
|
print(f"Total Messages Sent: {total_sent}")
|
||
|
|
print(f"Total Messages Rcvd: {total_received}")
|
||
|
|
print(f"Message Loss: {message_loss} ({loss_rate:.2f}%)")
|
||
|
|
print(f"Actual Throughput: {throughput:.2f} msg/sec")
|
||
|
|
print("-"*80)
|
||
|
|
|
||
|
|
sorted_latencies = sorted(latencies)
|
||
|
|
avg_latency_ms = statistics.mean(sorted_latencies) * 1000
|
||
|
|
min_latency_ms = sorted_latencies[0] * 1000
|
||
|
|
max_latency_ms = sorted_latencies[-1] * 1000
|
||
|
|
p50_latency_ms = statistics.median(sorted_latencies) * 1000
|
||
|
|
p95_latency_ms = sorted_latencies[int(len(sorted_latencies) * 0.95)] * 1000
|
||
|
|
p99_latency_ms = sorted_latencies[int(len(sorted_latencies) * 0.99)] * 1000
|
||
|
|
|
||
|
|
print("Latency Statistics (ms):")
|
||
|
|
print(f" Average: {avg_latency_ms:.4f} ms")
|
||
|
|
print(f" Min: {min_latency_ms:.4f} ms")
|
||
|
|
print(f" Max: {max_latency_ms:.4f} ms")
|
||
|
|
print(f" Median (p50): {p50_latency_ms:.4f} ms")
|
||
|
|
print(f" 95th Percentile: {p95_latency_ms:.4f} ms")
|
||
|
|
print(f" 99th Percentile: {p99_latency_ms:.4f} ms")
|
||
|
|
print("="*80)
|
||
|
|
|
||
|
|
async def main():
|
||
|
|
print("Starting WebSocket Pub/Sub Load Test...")
|
||
|
|
print(f"Simulating {NUM_SUBSCRIBERS} subscribers and {NUM_PUBLISHERS} publishers.")
|
||
|
|
print(f"Publishing at ~{NUM_PUBLISHERS * MESSAGES_PER_SECOND_PER_PUBLISHER} msg/sec for {TEST_DURATION_S} seconds.")
|
||
|
|
print("-"*80)
|
||
|
|
|
||
|
|
subscriber_tasks = [asyncio.create_task(subscriber_client(i)) for i in range(NUM_SUBSCRIBERS)]
|
||
|
|
publisher_tasks = [asyncio.create_task(publisher_client(i)) for i in range(NUM_PUBLISHERS)]
|
||
|
|
all_tasks = subscriber_tasks + publisher_tasks
|
||
|
|
|
||
|
|
try:
|
||
|
|
await asyncio.sleep(TEST_DURATION_S)
|
||
|
|
finally:
|
||
|
|
print("\nTest duration finished. Shutting down clients...")
|
||
|
|
for task in all_tasks:
|
||
|
|
task.cancel()
|
||
|
|
|
||
|
|
await asyncio.gather(*all_tasks, return_exceptions=True)
|
||
|
|
print_report()
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
asyncio.run(main())
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print("\nTest interrupted by user.")
|
||
|
|
|