#!/usr/bin/env python3 """HTTP benchmarks: Pure's document servers against axum (Rust) and drogon (C++) services with the same contract, on the same machine, with the same load generator. Servers are pinned to CPUs 0-2 and the load to CPU 3; stores live under --directory (use the device you care about: fsync costs are the device's). Prints one JSON object per (server, workload, round) and a summary table of medians over rounds. The load generator has one core, so near its limit the servers' CPU time tells them apart: 2xx/cpu-s is answers per second of server CPU (all threads, user and system). Durable writes are only comparable on a device in the same state. An SD card slows down under sustained syncs, so the order of runs alone can change a result several times over. Each round therefore runs the servers in a rotated order, each on a fresh store, and every write workload first times the device (an in-place 4 KiB write + fdatasync); the table shows that probe next to each result. --suite app serves the scoreboard of scores.aup (built by aup) with pure-serve, against the same scoreboard written by hand in axum and drogon: durable score posts, a sum and a page of 20 rows as JSON. """ import argparse import json import os import re import select import shutil import signal import statistics import struct import subprocess import sys import tempfile import time import http.client from pathlib import Path HERE = Path(__file__).resolve().parent PURE = HERE.parent.parent / 'prototypes' / 'connections' def start(args): process = subprocess.Popen(['taskset', '-c', '0-2'] + [str(a) for a in args], stdout=subprocess.PIPE, stderr=subprocess.PIPE) ready, _, _ = select.select([process.stdout], [], [], 20) assert ready, 'server did not start' port = json.loads(process.stdout.readline())['port'] return process, port def cpu_seconds(pid): """User and system time of every thread of the process.""" fields = Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split() return (int(fields[11]) + int(fields[12])) / os.sysconf('SC_CLK_TCK') def rss_kib(pid, field): for line in Path(f'/proc/{pid}/status').read_text().splitlines(): if line.startswith(field): return int(line.split()[1]) return 0 def put(port, key, body, expected): connection = http.client.HTTPConnection('127.0.0.1', port, timeout=30) headers = {'Content-Length': str(len(body) + 8)} headers['If-None-Match' if expected == 0 else 'If-Match'] = '*' if expected == 0 else f'"{expected}"' connection.request('PUT', f'/objects/{key}', struct.pack('9s} {'2xx/cpu-s':>10s} {'p50 us':>8s} {'p99 us':>8s} " f"{'errors':>7s} {'non-2xx':>8s} {'probe ms':>9s}") for label in dict.fromkeys(r['workload'] for r in results): for name in chosen: rows = [r for r in results if r['server'] == name and r['workload'] == label] if not rows: continue probes = [r['probe_ms'] for r in rows if r['probe_ms'] is not None] probe = f"{min(probes):.1f}-{max(probes):.1f}" if probes else '-' print(f"{label:32s} {name:14s} {median(rows, 'ok_per_second'):9.0f} " f"{median(rows, 'ok_per_cpu_second'):10.0f} {median(rows, 'p50_us'):8.0f} " f"{median(rows, 'p99_us'):8.0f} {sum(r['errors'] for r in rows):7d} " f"{sum(r['4xx'] + r['5xx'] for r in rows):8d} {probe:>9s}") print() for name in chosen: rows = [r for r in results if r['server'] == name] if rows: print(f"{name:14s} idle RSS {median(rows, 'idle_rss_kib'):.0f} KiB, " f"peak RSS {max(r['peak_rss_kib'] for r in rows)} KiB") if __name__ == '__main__': sys.exit(main())