Files
andros 80a0cebb63 Switch to Uvicorn and automate benchmark with Playwright in Docker
Server: replace Daphne with Uvicorn across Dockerfile, compose.yaml,
requirements.txt and INSTALLED_APPS. Uvicorn with the websockets worker
is noticeably faster for the pure-Python frameworks in this repo.

Benchmark: rewrite run_performance_tests.py as a real Playwright-driven
harness. Add a bench service (Dockerfile.bench + compose profile) that
runs headless Chromium against the web container, wipes the DB between
implementations via /_bench/clear/, discards two warmup iterations, then
measures 10 real iterations per framework. HTTP bodies are captured via
response.body(); WebSocket frames via page.on("websocket").

Visualization: new generate_plotly_plots.py emits the four comparison
PNGs with Plotly + Kaleido. The stale compile_performance_data.py
(hard-coded fake data) and the matplotlib script it fed are removed.

Cleanup: .gitignore now excludes performance_results_*.csv and logs/;
prior measurement CSVs and obsolete plot variants have been deleted.

README: describe the new Docker-only workflow and drop the outdated
manual-DevTools section.
2026-04-14 09:40:15 +02:00

166 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
Generate Plotly PNG charts from the most recent performance_results_*.csv.
Produces four charts that mirror the originals shipped with the article:
- plot_response_time.png
- plot_network_requests.png
- plot_data_transfer.png
- plot_stability.png
Usage (inside bench container):
python generate_plotly_plots.py
Outputs land in /bench (mounted to the project root).
"""
import csv
import glob
from collections import defaultdict
from pathlib import Path
from statistics import mean
import plotly.graph_objects as go
PALETTE = ['#3273dc', '#48c774', '#ffdd57', '#f14668', '#9b59b6', '#00d1b2']
LOWER_IS_BETTER = dict(
x=0.98, y=0.98, xref='paper', yref='paper',
text='\u2b07 Lower is Better',
showarrow=False,
bgcolor='wheat', opacity=0.75,
font=dict(size=12),
bordercolor='black', borderwidth=1, borderpad=6,
xanchor='right', yanchor='top',
)
LAYOUT_COMMON = dict(
plot_bgcolor='white',
paper_bgcolor='white',
font=dict(family='Inter, system-ui, sans-serif', size=14),
margin=dict(l=60, r=30, t=70, b=60),
yaxis=dict(gridcolor='#e5e5e5', zerolinecolor='#d0d0d0'),
xaxis=dict(gridcolor='#e5e5e5'),
)
def load_latest_csv() -> tuple[dict, str]:
files = glob.glob('performance_results_*.csv')
if not files:
raise SystemExit('No performance_results_*.csv found. Run the benchmark first.')
latest = max(files, key=lambda x: Path(x).stat().st_mtime)
print(f'Loading {latest}')
data = defaultdict(list)
with open(latest) as f:
for row in csv.DictReader(f):
if row.get('error') or int(row['iteration']) < 0:
continue
data[row['implementation']].append({
'iteration': int(row['iteration']),
'duration_ms': float(row['duration_ms']),
'http_requests': int(row.get('http_requests') or 0),
'response_bytes': int(row.get('response_bytes') or 0),
})
return dict(data), latest
def sort_by_speed(data: dict) -> dict:
return dict(sorted(data.items(), key=lambda kv: mean([m['duration_ms'] for m in kv[1]])))
def bar_chart(data, title, value_fn, label_fn, yaxis_title, output):
impls = list(data.keys())
values = [value_fn(data[i]) for i in impls]
labels = [label_fn(v) for v in values]
colors = [PALETTE[i % len(PALETTE)] for i in range(len(impls))]
fig = go.Figure(go.Bar(
x=impls, y=values,
text=labels, textposition='outside',
marker=dict(color=colors, line=dict(color='#222', width=1)),
))
fig.update_layout(
title=dict(text=title, font=dict(size=18, color='#222'), x=0.5, xanchor='center'),
yaxis_title=yaxis_title,
annotations=[LOWER_IS_BETTER],
**LAYOUT_COMMON,
)
# Headroom so the outside labels don't clip the title/annotation.
ymax = max(values) if values else 0
fig.update_yaxes(range=[0, ymax * 1.22 if ymax > 0 else 1])
fig.write_image(output, width=1000, height=600, scale=2)
print(f'\u2713 {output}')
def response_time_chart(data):
bar_chart(
data,
title='Create Alert - Average Response Time',
value_fn=lambda rows: mean(r['duration_ms'] for r in rows),
label_fn=lambda v: f'{v:.2f} ms',
yaxis_title='Duration (ms)',
output='plot_response_time.png',
)
def network_requests_chart(data):
bar_chart(
data,
title='HTTP Requests per Action',
value_fn=lambda rows: mean(r['http_requests'] for r in rows),
label_fn=lambda v: f'{v:.1f}',
yaxis_title='HTTP requests per action',
output='plot_network_requests.png',
)
def data_transfer_chart(data):
bar_chart(
data,
title='Data Transfer per Action',
value_fn=lambda rows: mean(r['response_bytes'] for r in rows) / 1024,
label_fn=lambda v: f'{v:.2f} KB',
yaxis_title='Data transfer (KB)',
output='plot_data_transfer.png',
)
def stability_chart(data):
fig = go.Figure()
for idx, (impl, rows) in enumerate(data.items()):
rows_sorted = sorted(rows, key=lambda r: r['iteration'])
fig.add_trace(go.Scatter(
x=[r['iteration'] + 1 for r in rows_sorted],
y=[r['duration_ms'] for r in rows_sorted],
mode='lines+markers',
name=impl,
line=dict(color=PALETTE[idx % len(PALETTE)], width=2),
marker=dict(size=8),
))
fig.update_layout(
title=dict(text='Performance Stability Across Iterations', font=dict(size=18, color='#222'), x=0.5, xanchor='center'),
xaxis_title='Iteration',
yaxis_title='Duration (ms)',
legend=dict(bgcolor='rgba(255,255,255,0.85)', bordercolor='#999', borderwidth=1),
annotations=[{**LOWER_IS_BETTER, 'text': '\u2b07 Lower & Flatter is Better'}],
**LAYOUT_COMMON,
)
fig.write_image('plot_stability.png', width=1100, height=600, scale=2)
print('\u2713 plot_stability.png')
def main() -> int:
data, _ = load_latest_csv()
if not data:
print('No measurements.')
return 1
data = sort_by_speed(data)
print('Order fastest→slowest:', ', '.join(data.keys()))
response_time_chart(data)
network_requests_chart(data)
data_transfer_chart(data)
stability_chart(data)
return 0
if __name__ == '__main__':
raise SystemExit(main())