aboutsummaryrefslogtreecommitdiffstats
path: root/benchmarks/bank
diff options
context:
space:
mode:
Diffstat (limited to 'benchmarks/bank')
-rw-r--r--benchmarks/bank/bank.aup64
-rw-r--r--benchmarks/bank/run.py117
-rw-r--r--benchmarks/bank/sqlite-bank.c176
3 files changed, 357 insertions, 0 deletions
diff --git a/benchmarks/bank/bank.aup b/benchmarks/bank/bank.aup
new file mode 100644
index 0000000..acc7938
--- /dev/null
+++ b/benchmarks/bank/bank.aup
@@ -0,0 +1,64 @@
+# A bank as pgbench's TPC-B-like transaction: accounts, tellers and branches,
+# each a list kept in key order, and a history of every transfer. A transfer
+# moves a teller's delta into an account, the teller and its branch, and
+# records it, all in one event: one transaction over four tables.
+record Account {
+ aid = 0
+ bid = 0
+ balance = 0
+ filler: text
+}
+record Teller {
+ tid = 0
+ bid = 0
+ balance = 0
+}
+record Branch {
+ bid = 0
+ balance = 0
+}
+record Transfer {
+ tid = 0
+ bid = 0
+ aid = 0
+ delta = 0
+ at = 0
+}
+
+state accounts: list of Account by aid
+state tellers: list of Teller by tid
+state branches: list of Branch by bid
+state history: list of Transfer
+
+input branch = message {
+ bid: u32
+}
+input teller = message {
+ tid: u32
+ bid: u32
+}
+input account = message {
+ aid: u32
+ bid: u32
+}
+input transfer = message {
+ aid: u32
+ tid: u32
+ bid: u32
+ delta: number
+}
+
+on branch { branches.add(Branch { bid: branch.bid }) }
+on teller { tellers.add(Teller { tid: teller.tid, bid: teller.bid }) }
+on account { accounts.add(Account { aid: account.aid, bid: account.bid, filler: "" }) }
+
+on transfer {
+ accounts[transfer.aid].balance = accounts[transfer.aid].balance + transfer.delta
+ tellers[transfer.tid].balance = tellers[transfer.tid].balance + transfer.delta
+ branches[transfer.bid].balance = branches[transfer.bid].balance + transfer.delta
+ history.append(Transfer { tid: transfer.tid, bid: transfer.bid, aid: transfer.aid, delta: transfer.delta, at: now })
+}
+
+view accounts_count = accounts.count
+view history_count = history.count
+view total = sum(b.balance for b in branches)
diff --git a/benchmarks/bank/run.py b/benchmarks/bank/run.py
new file mode 100644
index 0000000..218c8ec
--- /dev/null
+++ b/benchmarks/bank/run.py
@@ -0,0 +1,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()
diff --git a/benchmarks/bank/sqlite-bank.c b/benchmarks/bank/sqlite-bank.c
new file mode 100644
index 0000000..e4ab48f
--- /dev/null
+++ b/benchmarks/bank/sqlite-bank.c
@@ -0,0 +1,176 @@
+#define _GNU_SOURCE
+/* pgbench's TPC-B-like transaction on SQLite, the same bank as bank.aup for
+ * comparing with Pure (benchmarks/bank/run.py):
+ *
+ * sqlite-bank DATABASE ACCOUNTS THREADS SECONDS full|normal|off
+ *
+ * makes pgbench's tables (a branch, 10 tellers and 100,000 accounts a scale;
+ * ACCOUNTS, at least one scale), then THREADS threads, each with its own
+ * connection, run the transaction for SECONDS: BEGIN IMMEDIATE; UPDATE
+ * accounts; SELECT the balance; UPDATE tellers; UPDATE branches; INSERT into
+ * history; COMMIT, with random ids and delta as pgbench picks them. WAL, with
+ * synchronous FULL (each commit synced), NORMAL or OFF. Prints one JSON line. */
+#include <pthread.h>
+#include <sqlite3.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+static const char *database, *synchronous;
+static unsigned accounts, scale;
+static double seconds;
+static atomic_uint_fast64_t done, failed;
+enum { BUCKETS = 64 * 16 };
+static atomic_uint_fast64_t histogram[BUCKETS];
+
+static uint64_t now_us(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (uint64_t)t.tv_sec * 1000000 + (uint64_t)t.tv_nsec / 1000;
+}
+
+/* Log-linear buckets: 16 a power of two. */
+static unsigned bucket(uint64_t us) {
+ if (us < 16) return (unsigned)us;
+ unsigned power = 63 - (unsigned)__builtin_clzll(us);
+ return (power - 3) * 16 + (unsigned)((us >> (power - 4)) & 15);
+}
+static uint64_t bucket_value(unsigned index) {
+ if (index < 16) return index;
+ unsigned power = index / 16 + 3;
+ return ((uint64_t)16 + index % 16) << (power - 4);
+}
+static uint64_t percentile(uint64_t total, double p) {
+ uint64_t want = (uint64_t)((double)total * p), seen = 0;
+ for (unsigned i = 0; i < BUCKETS; ++i) {
+ seen += histogram[i];
+ if (seen > want) return bucket_value(i);
+ }
+ return 0;
+}
+
+static sqlite3 *open_database(void) {
+ sqlite3 *db;
+ char pragmas[160];
+ if (sqlite3_open(database, &db) != SQLITE_OK) { fprintf(stderr, "%s\n", sqlite3_errmsg(db)); exit(1); }
+ sqlite3_busy_timeout(db, 60000);
+ snprintf(pragmas, sizeof(pragmas), "PRAGMA journal_mode=WAL; PRAGMA synchronous=%s;", synchronous);
+ if (sqlite3_exec(db, pragmas, NULL, NULL, NULL) != SQLITE_OK) { fprintf(stderr, "%s\n", sqlite3_errmsg(db)); exit(1); }
+ return db;
+}
+
+static void run(sqlite3 *db, const char *sql) {
+ char *error = NULL;
+ if (sqlite3_exec(db, sql, NULL, NULL, &error) != SQLITE_OK) { fprintf(stderr, "%s: %s\n", sql, error); exit(1); }
+}
+
+static sqlite3_stmt *prepare(sqlite3 *db, const char *sql) {
+ sqlite3_stmt *statement;
+ if (sqlite3_prepare_v2(db, sql, -1, &statement, NULL) != SQLITE_OK) { fprintf(stderr, "%s\n", sqlite3_errmsg(db)); exit(1); }
+ return statement;
+}
+
+static uint64_t next_random(uint64_t *state) {
+ uint64_t x = *state;
+ x ^= x << 13, x ^= x >> 7, x ^= x << 17;
+ return *state = x;
+}
+
+static bool step_done(sqlite3_stmt *statement) {
+ int result = sqlite3_step(statement);
+ sqlite3_reset(statement);
+ return result == SQLITE_DONE || result == SQLITE_ROW;
+}
+
+static void *client(void *argument) {
+ uint64_t random = 0x9e3779b97f4a7c15u * (uintptr_t)argument;
+ sqlite3 *db = open_database();
+ sqlite3_stmt *begin = prepare(db, "BEGIN IMMEDIATE"), *commit = prepare(db, "COMMIT"),
+ *rollback = prepare(db, "ROLLBACK"),
+ *account = prepare(db, "UPDATE pgbench_accounts SET abalance = abalance + ?1 WHERE aid = ?2"),
+ *balance = prepare(db, "SELECT abalance FROM pgbench_accounts WHERE aid = ?1"),
+ *teller = prepare(db, "UPDATE pgbench_tellers SET tbalance = tbalance + ?1 WHERE tid = ?2"),
+ *branch = prepare(db, "UPDATE pgbench_branches SET bbalance = bbalance + ?1 WHERE bid = ?2"),
+ *history = prepare(db, "INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) "
+ "VALUES (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP)");
+ uint64_t stop = now_us() + (uint64_t)(seconds * 1e6);
+ while (now_us() < stop) {
+ int aid = (int)(next_random(&random) % accounts) + 1, tid = (int)(next_random(&random) % (10 * scale)) + 1,
+ bid = (int)(next_random(&random) % scale) + 1, delta = (int)(next_random(&random) % 10001) - 5000;
+ uint64_t started = now_us();
+ sqlite3_bind_int(account, 1, delta), sqlite3_bind_int(account, 2, aid);
+ sqlite3_bind_int(balance, 1, aid);
+ sqlite3_bind_int(teller, 1, delta), sqlite3_bind_int(teller, 2, tid);
+ sqlite3_bind_int(branch, 1, delta), sqlite3_bind_int(branch, 2, bid);
+ sqlite3_bind_int(history, 1, tid), sqlite3_bind_int(history, 2, bid), sqlite3_bind_int(history, 3, aid),
+ sqlite3_bind_int(history, 4, delta);
+ bool ok = step_done(begin) && step_done(account) && step_done(balance) && step_done(teller) &&
+ step_done(branch) && step_done(history) && step_done(commit);
+ if (!ok) {
+ step_done(rollback);
+ ++failed;
+ continue;
+ }
+ ++done;
+ ++histogram[bucket(now_us() - started)];
+ }
+ sqlite3_stmt *statements[] = {begin, commit, rollback, account, balance, teller, branch, history};
+ for (size_t i = 0; i < sizeof(statements) / sizeof(*statements); ++i) sqlite3_finalize(statements[i]);
+ sqlite3_close(db);
+ return NULL;
+}
+
+int main(int argc, char **argv) {
+ if (argc != 6) {
+ fputs("sqlite-bank DATABASE ACCOUNTS THREADS SECONDS full|normal|off\n", stderr);
+ return 2;
+ }
+ database = argv[1];
+ accounts = (unsigned)strtoul(argv[2], NULL, 10);
+ unsigned threads = (unsigned)strtoul(argv[3], NULL, 10);
+ seconds = atof(argv[4]);
+ synchronous = argv[5];
+ scale = accounts / 100000 ? accounts / 100000 : 1;
+ uint64_t began = now_us();
+ sqlite3 *db = open_database();
+ run(db, "DROP TABLE IF EXISTS pgbench_accounts; DROP TABLE IF EXISTS pgbench_tellers;"
+ "DROP TABLE IF EXISTS pgbench_branches; DROP TABLE IF EXISTS pgbench_history;"
+ "CREATE TABLE pgbench_accounts (aid INTEGER PRIMARY KEY, bid INTEGER, abalance INTEGER, filler CHAR(84));"
+ "CREATE TABLE pgbench_tellers (tid INTEGER PRIMARY KEY, bid INTEGER, tbalance INTEGER, filler CHAR(84));"
+ "CREATE TABLE pgbench_branches (bid INTEGER PRIMARY KEY, bbalance INTEGER, filler CHAR(88));"
+ "CREATE TABLE pgbench_history (tid INTEGER, bid INTEGER, aid INTEGER, delta INTEGER, mtime TIMESTAMP,"
+ " filler CHAR(22));");
+ run(db, "BEGIN");
+ sqlite3_stmt *insert = prepare(db, "INSERT INTO pgbench_branches (bid, bbalance, filler) VALUES (?1, 0, '')");
+ for (unsigned b = 1; b <= scale; ++b) sqlite3_bind_int(insert, 1, (int)b), step_done(insert);
+ sqlite3_finalize(insert);
+ insert = prepare(db, "INSERT INTO pgbench_tellers (tid, bid, tbalance, filler) VALUES (?1, ?2, 0, '')");
+ for (unsigned t = 1; t <= 10 * scale; ++t) sqlite3_bind_int(insert, 1, (int)t), sqlite3_bind_int(insert, 2, (int)((t - 1) / 10 + 1)), step_done(insert);
+ sqlite3_finalize(insert);
+ insert = prepare(db, "INSERT INTO pgbench_accounts (aid, bid, abalance, filler) VALUES (?1, ?2, 0, '')");
+ for (unsigned a = 1; a <= accounts; ++a)
+ sqlite3_bind_int(insert, 1, (int)a), sqlite3_bind_int(insert, 2, (int)((a - 1) / 100000 + 1)), step_done(insert);
+ sqlite3_finalize(insert);
+ run(db, "COMMIT; PRAGMA wal_checkpoint(TRUNCATE);");
+ double setup = (double)(now_us() - began) / 1e6;
+ pthread_t *thread = calloc(threads, sizeof(*thread));
+ uint64_t start = now_us();
+ for (unsigned i = 0; i < threads; ++i) pthread_create(&thread[i], NULL, client, (void *)(uintptr_t)(i + 1));
+ for (unsigned i = 0; i < threads; ++i) pthread_join(thread[i], NULL);
+ double elapsed = (double)(now_us() - start) / 1e6;
+ sqlite3_stmt *count = prepare(db, "SELECT count(*) FROM pgbench_history");
+ sqlite3_step(count);
+ long long history = sqlite3_column_int64(count, 0);
+ sqlite3_finalize(count);
+ sqlite3_close(db);
+ printf("{\"sqlite\":\"%s\",\"synchronous\":\"%s\",\"accounts\":%u,\"threads\":%u,\"setup_s\":%.2f,"
+ "\"transfers_per_second\":%.0f,\"p50_us\":%llu,\"p99_us\":%llu,\"failed\":%llu,\"history\":%lld}\n",
+ sqlite3_libversion(), synchronous, accounts, threads, setup, (double)done / elapsed,
+ (unsigned long long)percentile(done, 0.5), (unsigned long long)percentile(done, 0.99),
+ (unsigned long long)failed, history);
+ return history == (long long)done ? 0 : 1;
+}