1#!/usr/bin/env python32"""Measure local SSD read bandwidth at expert-granularity (random reads) and sequential reads,3bypassing the page cache with F_NOCACHE (macOS). Writes a JSON record to benchmarks/raw/<out>.4Usage: ssd_bandwidth.py <file> --chunk-mb 3.4 --n 200 --out benchmarks/raw/gateA/ssd/<host>.json5"""6import argparse, fcntl, json, os, platform, random, socket, subprocess, time, statistics, sys78F_NOCACHE = 48 # from <sys/fcntl.h> on macOS910def read_chunks(path, offsets, size, nocache=True):11 fd = os.open(path, os.O_RDONLY)12 if nocache:13 fcntl.fcntl(fd, F_NOCACHE, 1)14 lat = []15 nbytes = 016 t0 = time.perf_counter()17 for off in offsets:18 t = time.perf_counter()19 b = os.pread(fd, size, off)20 lat.append(time.perf_counter() - t)21 nbytes += len(b)22 tot = time.perf_counter() - t023 os.close(fd)24 return nbytes, tot, lat2526def main():27 ap = argparse.ArgumentParser()28 ap.add_argument("file")29 ap.add_argument("--chunk-mb", type=float, default=3.4)30 ap.add_argument("--n", type=int, default=200)31 ap.add_argument("--seq-mb", type=int, default=2048)32 ap.add_argument("--seed", type=int, default=0)33 ap.add_argument("--out", required=True)34 a = ap.parse_args()35 size = int(a.chunk_mb * 1024 * 1024)36 fsize = os.path.getsize(a.file)37 rng = random.Random(a.seed)38 offsets = [rng.randrange(0, fsize - size) // 4096 * 4096 for _ in range(a.n)]39 rec = {"host": socket.gethostname(), "file": a.file, "file_bytes": fsize, "chunk_bytes": size, "n": a.n,40 "seed": a.seed, "machine": platform.machine(), "macos": platform.mac_ver()[0],41 "chip": subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, text=True).stdout.strip(),42 "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"), "cmd": " ".join(sys.argv)}43 # random expert-sized reads, uncached44 nb, tot, lat = read_chunks(a.file, offsets, size, nocache=True)45 lat_ms = sorted(x * 1e3 for x in lat)46 rec["random_nocache"] = {"GBps": nb / tot / 1e9, "lat_ms_p50": statistics.median(lat_ms),47 "lat_ms_p95": lat_ms[int(0.95 * len(lat_ms)) - 1], "lat_ms_p99": lat_ms[int(0.99 * len(lat_ms)) - 1],48 "lat_ms_mean": statistics.mean(lat_ms)}49 # same offsets, page-cache allowed (second pass is warm)50 read_chunks(a.file, offsets, size, nocache=False)51 nb, tot, lat = read_chunks(a.file, offsets, size, nocache=False)52 rec["random_cached"] = {"GBps": nb / tot / 1e9, "lat_ms_p50": statistics.median(x * 1e3 for x in lat)}53 # sequential uncached54 seq = min(a.seq_mb * 1024 * 1024, fsize)55 offs = list(range(0, seq, 8 * 1024 * 1024))56 nb, tot, _ = read_chunks(a.file, offs, 8 * 1024 * 1024, nocache=True)57 rec["sequential_nocache_8MB"] = {"GBps": nb / tot / 1e9, "bytes": nb}58 os.makedirs(os.path.dirname(a.out), exist_ok=True)59 with open(a.out, "w") as f:60 json.dump(rec, f, indent=2)61 print(json.dumps(rec, indent=2))6263if __name__ == "__main__":64 main()65