aboutsummaryrefslogtreecommitdiffstats
path: root/research
diff options
context:
space:
mode:
Diffstat (limited to 'research')
-rw-r--r--research/experiments/010-one-sync-commit/2026-09-26-ext4-linux-aarch64.json148
-rw-r--r--research/experiments/010-one-sync-commit/commits.c47
-rw-r--r--research/experiments/010-one-sync-commit/probe.c70
-rw-r--r--research/experiments/010-one-sync-commit/run.py63
4 files changed, 328 insertions, 0 deletions
diff --git a/research/experiments/010-one-sync-commit/2026-09-26-ext4-linux-aarch64.json b/research/experiments/010-one-sync-commit/2026-09-26-ext4-linux-aarch64.json
new file mode 100644
index 0000000..a78f512
--- /dev/null
+++ b/research/experiments/010-one-sync-commit/2026-09-26-ext4-linux-aarch64.json
@@ -0,0 +1,148 @@
+{
+ "rounds": [
+ {
+ "probe": [
+ {
+ "way": "append, fsync, head, fsync (two syncs)",
+ "p50_ms": 9.02,
+ "p90_ms": 11.11,
+ "max_ms": 64.61
+ },
+ {
+ "way": "append, head, fdatasync",
+ "p50_ms": 5.75,
+ "p90_ms": 8.26,
+ "max_ms": 60.96
+ },
+ {
+ "way": "append, head, fsync",
+ "p50_ms": 5.69,
+ "p90_ms": 8.39,
+ "max_ms": 13.53
+ },
+ {
+ "way": "overwrite written zeros, head, fdatasync",
+ "p50_ms": 2.44,
+ "p90_ms": 5.45,
+ "max_ms": 67.07
+ },
+ {
+ "way": "overwrite written zeros, head, fsync",
+ "p50_ms": 5.7,
+ "p90_ms": 8.33,
+ "max_ms": 88.51
+ }
+ ],
+ "commits": {
+ "two syncs": {
+ "commits_per_second": 103,
+ "p50_ms": 9.19,
+ "p90_ms": 11.15,
+ "p99_ms": 62.85
+ },
+ "one sync": {
+ "commits_per_second": 292,
+ "p50_ms": 2.48,
+ "p90_ms": 4.69,
+ "p99_ms": 56.77
+ }
+ }
+ },
+ {
+ "probe": [
+ {
+ "way": "append, fsync, head, fsync (two syncs)",
+ "p50_ms": 9.19,
+ "p90_ms": 10.95,
+ "max_ms": 62.65
+ },
+ {
+ "way": "append, head, fdatasync",
+ "p50_ms": 5.82,
+ "p90_ms": 8.55,
+ "max_ms": 58.98
+ },
+ {
+ "way": "append, head, fsync",
+ "p50_ms": 5.66,
+ "p90_ms": 8.67,
+ "max_ms": 59.21
+ },
+ {
+ "way": "overwrite written zeros, head, fdatasync",
+ "p50_ms": 2.53,
+ "p90_ms": 6.02,
+ "max_ms": 61.15
+ },
+ {
+ "way": "overwrite written zeros, head, fsync",
+ "p50_ms": 5.75,
+ "p90_ms": 8.68,
+ "max_ms": 8.87
+ }
+ ],
+ "commits": {
+ "one sync": {
+ "commits_per_second": 289,
+ "p50_ms": 2.44,
+ "p90_ms": 5.49,
+ "p99_ms": 53.59
+ },
+ "two syncs": {
+ "commits_per_second": 109,
+ "p50_ms": 8.82,
+ "p90_ms": 11.42,
+ "p99_ms": 61.67
+ }
+ }
+ },
+ {
+ "probe": [
+ {
+ "way": "append, fsync, head, fsync (two syncs)",
+ "p50_ms": 8.82,
+ "p90_ms": 11.09,
+ "max_ms": 63.3
+ },
+ {
+ "way": "append, head, fdatasync",
+ "p50_ms": 5.61,
+ "p90_ms": 9.51,
+ "max_ms": 78.07
+ },
+ {
+ "way": "append, head, fsync",
+ "p50_ms": 6.21,
+ "p90_ms": 8.28,
+ "max_ms": 8.43
+ },
+ {
+ "way": "overwrite written zeros, head, fdatasync",
+ "p50_ms": 2.41,
+ "p90_ms": 4.67,
+ "max_ms": 58.57
+ },
+ {
+ "way": "overwrite written zeros, head, fsync",
+ "p50_ms": 5.07,
+ "p90_ms": 7.71,
+ "max_ms": 14.71
+ }
+ ],
+ "commits": {
+ "two syncs": {
+ "commits_per_second": 94,
+ "p50_ms": 9.09,
+ "p90_ms": 12.2,
+ "p99_ms": 63.22
+ },
+ "one sync": {
+ "commits_per_second": 304,
+ "p50_ms": 2.46,
+ "p90_ms": 5.15,
+ "p99_ms": 19.8
+ }
+ }
+ }
+ ]
+}
diff --git a/research/experiments/010-one-sync-commit/commits.c b/research/experiments/010-one-sync-commit/commits.c
new file mode 100644
index 0000000..8bc5878
--- /dev/null
+++ b/research/experiments/010-one-sync-commit/commits.c
@@ -0,0 +1,47 @@
+#define _POSIX_C_SOURCE 200809L
+/* Commit latency of the tree store: COMMITS sequential 16-byte inserts at
+ * the start of one of 64 documents, as in the HTTP put benchmark.
+ * commits STORE [COMMITS] -> one JSON object. */
+#include "store.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+static double now_ms(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
+}
+static int order(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return x < y ? -1 : x > y;
+}
+int main(int argc, char **argv) {
+ if (argc < 2) return 2;
+ const char *path = argv[1];
+ int commits = argc > 2 ? atoi(argv[2]) : 200;
+ if (commits < 1 || pure_store_init(path) != PURE_OK) return 1;
+ int64_t revision[64] = {0};
+ char key[16];
+ const unsigned char body[16] = "0123456789abcdef";
+ for (int k = 0; k < 64; ++k) {
+ snprintf(key, sizeof(key), "k%d", k);
+ if (pure_store_put(path, key, 0, (PureSpan){body, 16}, &revision[k]) != PURE_OK) return 1;
+ }
+ double *t = malloc(sizeof(double) * (size_t)commits), start = now_ms();
+ if (!t) return 1;
+ for (int i = 0; i < commits; ++i) {
+ int k = i % 64;
+ snprintf(key, sizeof(key), "k%d", k);
+ double begun = now_ms();
+ if (pure_store_splice(path, key, revision[k], 0, 0, (PureSpan){body, 16}, PURE_OBJECT_LIMIT,
+ &revision[k]) != PURE_OK) return 1;
+ t[i] = now_ms() - begun;
+ }
+ double total = now_ms() - start;
+ qsort(t, (size_t)commits, sizeof(double), order);
+ printf("{\"commits_per_second\":%.0f,\"p50_ms\":%.2f,\"p90_ms\":%.2f,\"p99_ms\":%.2f}\n", commits / total * 1e3,
+ t[commits / 2], t[commits * 9 / 10], t[commits * 99 / 100]);
+ free(t);
+ return 0;
+}
diff --git a/research/experiments/010-one-sync-commit/probe.c b/research/experiments/010-one-sync-commit/probe.c
new file mode 100644
index 0000000..0569192
--- /dev/null
+++ b/research/experiments/010-one-sync-commit/probe.c
@@ -0,0 +1,70 @@
+#define _GNU_SOURCE
+/* What each way of making a small commit durable costs on one device: an
+ * 8 KiB commit plus a 64-byte head, 40 times per way. The ways run
+ * round-robin so device drift reaches all of them alike.
+ * probe DIRECTORY -> one JSON object per way (milliseconds). */
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+static double now_ms(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return t.tv_sec * 1e3 + t.tv_nsec / 1e6;
+}
+static int order(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return x < y ? -1 : x > y;
+}
+enum { WAYS = 5, ROUNDS = 40, REGION = 8192 };
+static const char *names[WAYS] = {
+ "append, fsync, head, fsync (two syncs)",
+ "append, head, fdatasync",
+ "append, head, fsync",
+ "overwrite written zeros, head, fdatasync",
+ "overwrite written zeros, head, fsync",
+};
+int main(int argc, char **argv) {
+ const char *directory = argc > 1 ? argv[1] : "/var/tmp";
+ char path[WAYS][512];
+ int fd[WAYS];
+ off_t end[WAYS];
+ static char region[REGION], head[64];
+ memset(region, 'r', sizeof(region));
+ for (int w = 0; w < WAYS; ++w) {
+ snprintf(path[w], sizeof(path[w]), "%s/pure-sync-probe-%d", directory, w);
+ fd[w] = open(path[w], O_RDWR | O_CREAT | O_TRUNC, 0600);
+ if (fd[w] < 0) return 1;
+ end[w] = 4096;
+ if (w >= 3) {
+ size_t bytes = (size_t)(ROUNDS + 2) * REGION + 4096;
+ char *zeros = calloc(1, bytes);
+ if (!zeros || pwrite(fd[w], zeros, bytes, 0) != (ssize_t)bytes) return 1;
+ free(zeros);
+ } else if (pwrite(fd[w], region, 4096, 0) != 4096) return 1;
+ fsync(fd[w]);
+ }
+ double t[WAYS][ROUNDS];
+ for (int r = 0; r < ROUNDS; ++r)
+ for (int w = 0; w < WAYS; ++w) {
+ head[0] = (char)r;
+ double start = now_ms();
+ if (pwrite(fd[w], region, REGION, end[w]) != REGION) return 1;
+ end[w] += REGION;
+ if (w == 0) fsync(fd[w]);
+ if (pwrite(fd[w], head, 64, (r % 2) * 64) != 64) return 1;
+ if (w == 1 || w == 3) fdatasync(fd[w]); else fsync(fd[w]);
+ t[w][r] = now_ms() - start;
+ }
+ for (int w = 0; w < WAYS; ++w) {
+ qsort(t[w], ROUNDS, sizeof(double), order);
+ printf("{\"way\":\"%s\",\"p50_ms\":%.2f,\"p90_ms\":%.2f,\"max_ms\":%.2f}\n", names[w], t[w][ROUNDS / 2],
+ t[w][ROUNDS * 9 / 10], t[w][ROUNDS - 1]);
+ close(fd[w]);
+ unlink(path[w]);
+ }
+ return 0;
+}
diff --git a/research/experiments/010-one-sync-commit/run.py b/research/experiments/010-one-sync-commit/run.py
new file mode 100644
index 0000000..9a01c07
--- /dev/null
+++ b/research/experiments/010-one-sync-commit/run.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+"""Commit cost on the store's device (see docs/research/076-one-sync-commit.md).
+
+Runs probe.c (the ways of making a small commit durable) and commits.c (tree
+store commit latency) built against the two-sync store of --before and the
+current one, alternating their order per round so device drift reaches both
+alike. Prints one JSON document.
+"""
+import argparse
+import json
+import shutil
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parents[2]
+PURE = ROOT / 'prototypes' / 'connections'
+
+
+def build(output, sources):
+ subprocess.run(['cc', '-O2', '-std=c17', f'-I{PURE}', '-o', str(output)] + [str(s) for s in sources] + ['-lpthread'],
+ check=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('--before', default='ad2197c', help='a revision with the two-sync commit')
+ parser.add_argument('--rounds', type=int, default=3)
+ parser.add_argument('--commits', type=int, default=200)
+ args = parser.parse_args()
+ with tempfile.TemporaryDirectory() as work:
+ work = Path(work)
+ before = work / 'store-tree-before.c'
+ before.write_text(subprocess.run(['git', '-C', str(ROOT), 'show', f'{args.before}:prototypes/connections/store-tree.c'],
+ capture_output=True, text=True, check=True).stdout)
+ build(work / 'probe', [HERE / 'probe.c'])
+ builds = {'two syncs': work / 'commits-before', 'one sync': work / 'commits-after'}
+ build(builds['two syncs'], [HERE / 'commits.c', before])
+ build(builds['one sync'], [HERE / 'commits.c', PURE / 'store-tree.c'])
+ results = {'rounds': []}
+ for round_ in range(args.rounds):
+ probe = subprocess.run([str(work / 'probe'), str(args.directory)], capture_output=True, text=True, check=True)
+ measured = {'probe': [json.loads(line) for line in probe.stdout.splitlines()], 'commits': {}}
+ names = list(builds) if round_ % 2 == 0 else list(builds)[::-1]
+ for name in names:
+ store = Path(tempfile.mkdtemp(dir=args.directory, prefix='pure-commit-cost-'))
+ shutil.rmtree(store)
+ try:
+ run = subprocess.run([str(builds[name]), str(store), str(args.commits)], capture_output=True, text=True,
+ check=True)
+ measured['commits'][name] = json.loads(run.stdout)
+ finally:
+ shutil.rmtree(store, ignore_errors=True)
+ 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())