99 lines
4.1 KiB
Python
99 lines
4.1 KiB
Python
"""100 virtual clients in disposable prod/venue; does not test Wi-Fi radios."""
|
|
import json
|
|
import math
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import statistics
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import uuid
|
|
|
|
sys.dont_write_bytecode = True
|
|
from flow import API, Stand, export_package, fixture
|
|
|
|
|
|
def check_load(stand):
|
|
data = fixture(stand, 'online')
|
|
venue = stand.apis['venue']
|
|
venue.request('/api/games/import', export_package(stand.apis['prod'], data['game']))
|
|
game = data['game']
|
|
teams = [venue.request('/api/teams', {'gameID': game, 'name': f'TEST load {i}', 'participantsCount': 4}) for i in range(25)]
|
|
venue.request(f'/api/games/{game}/start', {}, 'PUT')
|
|
samples = {}
|
|
lock = threading.Lock()
|
|
barrier = threading.Barrier(101)
|
|
ids = [str(uuid.uuid4()) for _ in range(100)]
|
|
connections = [str(uuid.uuid4()) for _ in range(100)]
|
|
|
|
def measured(name, action):
|
|
before = time.perf_counter()
|
|
result = action()
|
|
with lock:
|
|
samples.setdefault(name, []).append(time.perf_counter() - before)
|
|
return result
|
|
|
|
def client(index):
|
|
public = API(venue.base)
|
|
team = teams[index // 4]
|
|
heartbeat = {'deviceID': ids[index], 'connectionID': connections[index], 'teamID': team['ID'], 'password': team['password']}
|
|
barrier.wait(timeout=30)
|
|
started = time.monotonic()
|
|
next_heartbeat = next_story = started
|
|
moved = False
|
|
while time.monotonic() - started < 60:
|
|
now = time.monotonic()
|
|
if now >= next_heartbeat:
|
|
result = measured('heartbeat', lambda: public.request('/api/devices/heartbeat', heartbeat))
|
|
assert result['enabled'] is True
|
|
next_heartbeat += 20
|
|
if not moved and index % 4 == 0:
|
|
for code in ('p1', 'p2'):
|
|
measured('move', lambda code=code: public.move(team, code))
|
|
measured('answers', lambda: public.request(f'/api/teams/{team["ID"]}/answers', {'password': team['password'], 'answers': [{'questionCode': 'q1', 'answer': 'Butler'}]}))
|
|
moved = True
|
|
if now >= next_story:
|
|
measured('story', lambda: public.story(team))
|
|
next_story += 5
|
|
time.sleep(.05)
|
|
|
|
with ThreadPoolExecutor(max_workers=100) as pool:
|
|
futures = [pool.submit(client, index) for index in range(100)]
|
|
barrier.wait(timeout=30)
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline:
|
|
snapshot = measured('devices', lambda: venue.request(f'/api/devices?gameID={game}'))
|
|
assert len(snapshot['devices']) <= 100
|
|
time.sleep(5)
|
|
for future in futures:
|
|
future.result()
|
|
devices = venue.request(f'/api/devices?gameID={game}')['devices']
|
|
assert {device['ID'] for device in devices} == set(ids)
|
|
assert all(device['connected'] and len(device['connections']) == 1 for device in devices)
|
|
for team in teams:
|
|
assert len(API(venue.base).story(team)['actions']) == 2
|
|
answers = venue.request(f'/api/games/{game}/answers')['teams']
|
|
assert {row['team']['ID'] for row in answers if any(answer.get('answer') == 'Butler' for answer in row.get('answers', []))} == {team['ID'] for team in teams}
|
|
report = {}
|
|
for name, values in sorted(samples.items()):
|
|
values.sort()
|
|
p95 = values[math.ceil(.95 * len(values)) - 1]
|
|
report[name] = {'requests': len(values), 'p50_ms': round(statistics.median(values) * 1000), 'p95_ms': round(p95 * 1000), 'max_ms': round(values[-1] * 1000)}
|
|
assert p95 <= 2, f'{name} exceeds the test budget p95 <= 2s'
|
|
print('PASS: 100 profiles, 25 teams, 60s, 100 connected devices, 50 moves, 25 answers, zero HTTP errors', flush=True)
|
|
print(json.dumps(report, sort_keys=True), flush=True)
|
|
|
|
|
|
def main():
|
|
with tempfile.TemporaryDirectory(prefix='ed029-load-') as directory:
|
|
stand = Stand(directory)
|
|
try:
|
|
stand.start()
|
|
check_load(stand)
|
|
finally:
|
|
stand.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|