aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/013-parallel-syncs/run.py
blob: ec2f23032ee5a6236b2785179da43aec532b8705 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3
"""Does the device sync several stores at once? (docs/research/084-parallel-syncs.md)

1. probe.c: 1, 2 and 4 threads, each committing to a file of its own
   (bytes over zeros, fdatasync), the order rotated per round.
2. The scoreboard app (benchmarks/http/scores.aup) taking durable posts:
   one pure-serve owning one store under 64 connections, against two
   pure-serve, each owning a store of its own (what sharding by key would
   make), under 32 connections each at once. Servers on CPUs 0 and 1, load
   generators on 3 and 2.
Prints one JSON document; each round also goes to stderr as it ends.
"""
import argparse
import json
import select
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
HTTP = ROOT / 'benchmarks' / 'http'


def start(cpu, module, store):
    process = subprocess.Popen(['taskset', '-c', str(cpu), str(ROOT / 'build/pure-serve'), '--module', str(module),
                                '--store', str(store), '--port', '0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    ready, _, _ = select.select([process.stdout], [], [], 20)
    assert ready, 'pure-serve did not start'
    return process, json.loads(process.stdout.readline())['port']


def loads(targets, seconds):
    """Load generators at once, one per (cpu, port, connections); their JSON."""
    running = [subprocess.Popen(['taskset', '-c', str(cpu), str(HTTP / 'load'), str(port), str(connections),
                                 str(seconds), 'score', str(connections), 'keep-alive', '16'],
                                stdout=subprocess.PIPE, text=True) for cpu, port, connections in targets]
    return [json.loads(process.communicate()[0]) for process in running]


def owners(count, module, directory, seconds):
    stores = [Path(tempfile.mkdtemp(dir=directory, prefix='pure-sync-owner-')) for _ in range(count)]
    servers = []
    try:
        for i, store in enumerate(stores):
            shutil.rmtree(store)
            servers.append(start(i, module, store))
        measured = loads([(3 - i, port, 64 // count) for i, (_, port) in enumerate(servers)], seconds)
        return {'owners': count, 'posts_per_second': sum(m['2xx'] for m in measured) / seconds,
                'p50_us': max(m['p50_us'] for m in measured), 'p99_us': max(m['p99_us'] for m in measured),
                'errors': sum(m['errors'] + m['4xx'] + m['5xx'] for m in measured)}
    finally:
        for process, _ in servers:
            process.terminate()
            process.wait()
        for store in stores:
            shutil.rmtree(store, ignore_errors=True)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--directory', type=Path, default=Path('/var/tmp'), help='on the device to measure')
    parser.add_argument('--aup', type=Path, default=ROOT.parent / 'aup' / 'build' / 'aup')
    parser.add_argument('--rounds', type=int, default=3)
    parser.add_argument('--seconds', type=float, default=3)
    args = parser.parse_args()
    with tempfile.TemporaryDirectory() as work:
        work = Path(work)
        subprocess.run(['cc', '-O2', '-std=c17', '-pthread', '-o', str(work / 'probe'), str(HERE / 'probe.c')], check=True)
        subprocess.run(['cc', '-O2', '-std=c17', '-o', str(HTTP / 'load'), str(HTTP / 'load.c')], check=True)
        module = work / 'scores.pmod'
        subprocess.run([str(args.aup), 'build', str(HTTP / 'scores.aup'), '-o', str(module)], check=True)
        results = {'rounds': []}
        for round_ in range(args.rounds):
            measured = {'probe': [], 'app': []}
            threads = [1, 2, 4]
            threads = threads[round_ % 3:] + threads[:round_ % 3]
            for count in threads:
                probe = subprocess.run([str(work / 'probe'), str(args.directory), str(count), str(args.seconds), '4096'],
                                       capture_output=True, text=True, check=True)
                measured['probe'].append(json.loads(probe.stdout))
            for count in ((1, 2) if round_ % 2 == 0 else (2, 1)):
                measured['app'].append(owners(count, module, args.directory, args.seconds))
            results['rounds'].append(measured)
            print(json.dumps(measured), file=sys.stderr, flush=True)
        print(json.dumps(results, indent=1))


if __name__ == '__main__':
    sys.exit(main())