aboutsummaryrefslogtreecommitdiffstats
path: root/benchmarks/bank/run.py
blob: 218c8ec0ca2f808a44c34aaad458680c391315ac (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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
"""A bank served by pure-serve under pgbench's TPC-B-like transaction.

bank.aup keeps accounts, tellers and branches as lists in key order and a
history list; a transfer updates one row of each of the three and appends a
history row, in one event. For each --accounts count: a fresh store, the
branches, tellers and accounts of that scale made by posts (timed), then
--seconds of transfers from --connections clients (benchmarks/http/load
transfer), servers on cores 0-2 and the load on core 3. Prints one JSON line
per count: setup time, transfers a second, latency, and server CPU time per
transfer.
"""
import argparse
import http.client
import json
import os
import select
import shutil
import subprocess
import tempfile
import threading
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]


def cpu_seconds(pid):
    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 post_all(port, path, bodies, threads=8):
    """Each body posted once, from threads keep-alive connections."""
    failed = []

    def worker(part):
        connection = http.client.HTTPConnection('127.0.0.1', port, timeout=60)
        for body in part:
            connection.request('POST', path, body, {'Content-Type': 'application/json'})
            answer = connection.getresponse()
            answer.read()
            if answer.status != 200:
                failed.append((body, answer.status))
        connection.close()

    workers = [threading.Thread(target=worker, args=(bodies[i::threads],)) for i in range(threads)]
    for w in workers:
        w.start()
    for w in workers:
        w.join()
    assert not failed, failed[:3]


def run(serve, module, directory, accounts, seconds, connections, threads):
    store = directory / f'bank-{accounts}'
    shutil.rmtree(store, ignore_errors=True)
    process = subprocess.Popen(['taskset', '-c', '0-2', str(serve), '--module', str(module), '--store', str(store),
                                '--port', '0', '--threads', str(threads)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    try:
        select.select([process.stdout], [], [], 10)
        port = json.loads(process.stdout.readline())['port']
        scale = max(1, accounts // 100000)
        began = time.perf_counter()
        post_all(port, '/inputs/branch', [json.dumps({'bid': b + 1}).encode() for b in range(scale)], 1)
        post_all(port, '/inputs/teller', [json.dumps({'tid': t + 1, 'bid': t // 10 + 1}).encode()
                                          for t in range(10 * scale)], 1)
        post_all(port, '/inputs/account', [json.dumps({'aid': a + 1, 'bid': a // 100000 + 1}).encode()
                                           for a in range(accounts)])
        setup = time.perf_counter() - began
        used = cpu_seconds(process.pid)
        load = subprocess.run(['taskset', '-c', '3', str(ROOT / 'benchmarks' / 'http' / 'load'), str(port),
                               str(connections), str(seconds), 'transfer', str(accounts), 'keep-alive'],
                              capture_output=True, text=True, check=True)
        result = json.loads(load.stdout)
        used = cpu_seconds(process.pid) - used
        connection = http.client.HTTPConnection('127.0.0.1', port, timeout=30)
        connection.request('GET', '/views/history_count')
        history = int(connection.getresponse().read())
        connection.close()
        # Every transfer answered is in the history, and those in flight when the load stopped may be too.
        assert result['2xx'] <= history <= result['2xx'] + connections, (history, result)
        return {'accounts': accounts, 'setup_s': round(setup, 2), 'transfers_per_second': result['requests_per_second'],
                'p50_us': result['p50_us'], 'p99_us': result['p99_us'], 'errors': result['errors'],
                'non_2xx': result['completed'] - result['2xx'],
                'cpu_us_per_transfer': round(used / max(result['2xx'], 1) * 1e6, 1)}
    finally:
        process.terminate()
        process.communicate(timeout=60)
        shutil.rmtree(store, ignore_errors=True)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--aup', type=Path, default=ROOT.parent / 'aup' / 'build' / 'aup')
    parser.add_argument('--serve', type=Path, default=ROOT / 'build' / 'pure-serve')
    parser.add_argument('--directory', type=Path, default=Path('/dev/shm'))
    parser.add_argument('--accounts', default='1000,10000,30000')
    parser.add_argument('--seconds', type=float, default=5)
    parser.add_argument('--connections', type=int, default=16)
    parser.add_argument('--threads', type=int, default=1)
    args = parser.parse_args()
    work = Path(tempfile.mkdtemp(prefix='pure-bank-'))
    try:
        module = work / 'bank.pmod'
        subprocess.run([str(args.aup), 'build', str(HERE / 'bank.aup'), '-o', str(module)], check=True)
        for accounts in (int(a) for a in args.accounts.split(',')):
            print(json.dumps({'directory': str(args.directory), **run(args.serve, module, args.directory, accounts,
                                                                      args.seconds, args.connections, args.threads)}),
                  flush=True)
    finally:
        shutil.rmtree(work)


if __name__ == '__main__':
    main()