|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Benchmark: OneLocker vs ThreeLocker. |
| 3 | +
|
| 4 | +Run: python benchmark_lockers.py |
| 5 | +""" |
| 6 | + |
| 7 | +import multiprocessing |
| 8 | +import os |
| 9 | +import sys |
| 10 | +import tempfile |
| 11 | +import time |
| 12 | + |
| 13 | +multiprocessing.set_start_method("spawn", force=True) |
| 14 | + |
| 15 | +from ubiquerg import OneLocker, ThreeLocker |
| 16 | + |
| 17 | + |
| 18 | +def benchmark_exclusive_access(filepath, iterations=100): |
| 19 | + """Measure lock/unlock cycle speed for exclusive access.""" |
| 20 | + one = OneLocker(filepath) |
| 21 | + start = time.perf_counter() |
| 22 | + for _ in range(iterations): |
| 23 | + one.write_lock() |
| 24 | + one.write_unlock() |
| 25 | + one_time = time.perf_counter() - start |
| 26 | + |
| 27 | + three = ThreeLocker(filepath) |
| 28 | + start = time.perf_counter() |
| 29 | + for _ in range(iterations): |
| 30 | + three.write_lock() |
| 31 | + three.write_unlock() |
| 32 | + three_time = time.perf_counter() - start |
| 33 | + |
| 34 | + return one_time, three_time |
| 35 | + |
| 36 | + |
| 37 | +def reader_process(filepath, locker_type, hold_time, barrier, timestamps, idx): |
| 38 | + """Subprocess: wait for barrier, acquire read lock, hold it, record times.""" |
| 39 | + if locker_type == "three": |
| 40 | + locker = ThreeLocker(filepath) |
| 41 | + else: |
| 42 | + locker = OneLocker(filepath) |
| 43 | + |
| 44 | + barrier.wait() |
| 45 | + t_start = time.perf_counter() |
| 46 | + locker.read_lock() |
| 47 | + t_acquired = time.perf_counter() |
| 48 | + time.sleep(hold_time) |
| 49 | + locker.read_unlock() |
| 50 | + t_done = time.perf_counter() |
| 51 | + |
| 52 | + timestamps[idx] = (t_start, t_acquired, t_done) |
| 53 | + |
| 54 | + |
| 55 | +def benchmark_concurrent_reads(filepath, n_readers=4, hold_time=0.5): |
| 56 | + """Spawn n_readers behind a barrier, measure overlap behavior.""" |
| 57 | + results = {} |
| 58 | + |
| 59 | + for locker_type in ["one", "three"]: |
| 60 | + barrier = multiprocessing.Barrier(n_readers) |
| 61 | + manager = multiprocessing.Manager() |
| 62 | + timestamps = manager.dict() |
| 63 | + |
| 64 | + processes = [] |
| 65 | + for i in range(n_readers): |
| 66 | + p = multiprocessing.Process( |
| 67 | + target=reader_process, |
| 68 | + args=(filepath, locker_type, hold_time, barrier, timestamps, i), |
| 69 | + ) |
| 70 | + processes.append(p) |
| 71 | + |
| 72 | + for p in processes: |
| 73 | + p.start() |
| 74 | + for p in processes: |
| 75 | + p.join(timeout=60) |
| 76 | + |
| 77 | + starts = [timestamps[i][0] for i in range(n_readers)] |
| 78 | + acquireds = [timestamps[i][1] for i in range(n_readers)] |
| 79 | + dones = [timestamps[i][2] for i in range(n_readers)] |
| 80 | + |
| 81 | + wall = max(dones) - min(starts) |
| 82 | + avg_wait = sum(a - s for s, a in zip(starts, acquireds)) / n_readers |
| 83 | + |
| 84 | + results[locker_type] = {"wall": wall, "avg_wait": avg_wait} |
| 85 | + |
| 86 | + return results |
| 87 | + |
| 88 | + |
| 89 | +def main(): |
| 90 | + tmpdir = tempfile.mkdtemp() |
| 91 | + filepath = os.path.join(tmpdir, "benchmark.txt") |
| 92 | + with open(filepath, "w") as f: |
| 93 | + f.write("benchmark data") |
| 94 | + |
| 95 | + iterations = 100 |
| 96 | + n_readers = 4 |
| 97 | + hold_times = [0.5, 3.0] |
| 98 | + |
| 99 | + print("ubiquerg locker benchmark") |
| 100 | + print("=" * 50) |
| 101 | + |
| 102 | + # Benchmark 1: Exclusive access speed |
| 103 | + print(f"\n1. Exclusive lock/unlock ({iterations} iterations)") |
| 104 | + print("-" * 50) |
| 105 | + one_time, three_time = benchmark_exclusive_access(filepath, iterations) |
| 106 | + speedup = three_time / one_time |
| 107 | + print(f" OneLocker: {one_time:.4f}s ({one_time / iterations * 1000:.2f}ms/cycle)") |
| 108 | + print(f" ThreeLocker: {three_time:.4f}s ({three_time / iterations * 1000:.2f}ms/cycle)") |
| 109 | + print(f" OneLocker {speedup:.1f}x faster") |
| 110 | + |
| 111 | + # Benchmark 2: Concurrent readers at different hold times |
| 112 | + for hold_time in hold_times: |
| 113 | + print(f"\n2. Concurrent readers ({n_readers} readers, {hold_time}s hold each)") |
| 114 | + print("-" * 50) |
| 115 | + # Suppress wait_for_lock stdout dots (fd-level for child processes) |
| 116 | + sys.stdout.flush() |
| 117 | + devnull = os.open(os.devnull, os.O_WRONLY) |
| 118 | + old_fd = os.dup(1) |
| 119 | + os.dup2(devnull, 1) |
| 120 | + os.close(devnull) |
| 121 | + results = benchmark_concurrent_reads(filepath, n_readers, hold_time) |
| 122 | + os.dup2(old_fd, 1) |
| 123 | + os.close(old_fd) |
| 124 | + |
| 125 | + one = results["one"] |
| 126 | + three = results["three"] |
| 127 | + print(f" OneLocker: {one['wall']:.2f}s wall (avg {one['avg_wait']:.2f}s wait)") |
| 128 | + print(f" ThreeLocker: {three['wall']:.2f}s wall (avg {three['avg_wait']:.2f}s wait)") |
| 129 | + |
| 130 | + os.unlink(filepath) |
| 131 | + os.rmdir(tmpdir) |
| 132 | + |
| 133 | + |
| 134 | +if __name__ == "__main__": |
| 135 | + main() |
0 commit comments