aboutsummaryrefslogtreecommitdiffstats
path: root/research
diff options
context:
space:
mode:
Diffstat (limited to 'research')
-rw-r--r--research/experiments/009-runtime-drafts/2026-09-25-ext4-linux-aarch64.json69
-rw-r--r--research/experiments/009-runtime-drafts/bench.c120
-rwxr-xr-xresearch/experiments/009-runtime-drafts/run.py45
3 files changed, 234 insertions, 0 deletions
diff --git a/research/experiments/009-runtime-drafts/2026-09-25-ext4-linux-aarch64.json b/research/experiments/009-runtime-drafts/2026-09-25-ext4-linux-aarch64.json
new file mode 100644
index 0000000..e85ec38
--- /dev/null
+++ b/research/experiments/009-runtime-drafts/2026-09-25-ext4-linux-aarch64.json
@@ -0,0 +1,69 @@
+{
+ "device": "/dev/mmcblk0p2",
+ "filesystem": "ext4",
+ "machine": "aarch64",
+ "kernel": "6.18.39+rpt-rpi-2712",
+ "store": "store-tree.c",
+ "runs": [
+ {
+ "mode": "copy",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 0.06,
+ "save_median_ms": 10.083,
+ "save_p90_ms": 12.536,
+ "peak_rss_kib": 4496,
+ "final_revision": 25
+ },
+ {
+ "mode": "draft",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 3.18,
+ "save_median_ms": 9.245,
+ "save_p90_ms": 27.849,
+ "peak_rss_kib": 1664,
+ "final_revision": 25
+ },
+ {
+ "mode": "draft",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 3.07,
+ "save_median_ms": 8.643,
+ "save_p90_ms": 18.492,
+ "peak_rss_kib": 1648,
+ "final_revision": 25
+ },
+ {
+ "mode": "copy",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 0.04,
+ "save_median_ms": 10.528,
+ "save_p90_ms": 12.78,
+ "peak_rss_kib": 4480,
+ "final_revision": 25
+ },
+ {
+ "mode": "copy",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 0.04,
+ "save_median_ms": 13.571,
+ "save_p90_ms": 21.87,
+ "peak_rss_kib": 4464,
+ "final_revision": 25
+ },
+ {
+ "mode": "draft",
+ "document_bytes": 1048576,
+ "saves": 24,
+ "keystroke_median_us": 3.59,
+ "save_median_ms": 9.128,
+ "save_p90_ms": 50.847,
+ "peak_rss_kib": 1664,
+ "final_revision": 25
+ }
+ ]
+}
diff --git a/research/experiments/009-runtime-drafts/bench.c b/research/experiments/009-runtime-drafts/bench.c
new file mode 100644
index 0000000..e486d68
--- /dev/null
+++ b/research/experiments/009-runtime-drafts/bench.c
@@ -0,0 +1,120 @@
+/* Where an app's working value lives, measured on one store (store-tree.c).
+ *
+ * copy: the app loads its own copy into a gap buffer, types into it, and on
+ * save reads the stored value back, finds the changed range by
+ * comparing prefix and suffix, then submits that splice (the pattern
+ * the current terminal editor uses).
+ * draft: the runtime owns the working version; typing is a draft splice and
+ * save is a commit of the draft's new extents.
+ *
+ * Each save follows one typed character in the middle of the document.
+ * "create" makes the stored document in a separate process, so the peak RSS
+ * of copy and draft covers only the editing session. */
+#define _POSIX_C_SOURCE 200809L
+#include "state.h"
+#include "store.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+static double now_us(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t);
+ return (double)t.tv_sec * 1e6 + (double)t.tv_nsec / 1e3;
+}
+
+static int compare(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return (x > y) - (x < y);
+}
+
+/* Linux VmHWM belongs to this exec's address space; ru_maxrss would carry
+ * the launching interpreter's peak across exec. */
+static long peak_kib(void) {
+ FILE *file = fopen("/proc/self/status", "r");
+ char line[256];
+ long value = -1;
+ while (file && fgets(line, sizeof(line), file))
+ if (!strncmp(line, "VmHWM:", 6)) value = strtol(line + 6, NULL, 10);
+ if (file) fclose(file);
+ return value;
+}
+
+typedef struct { unsigned char *text; size_t capacity, gap_start, gap_end; } Gap;
+
+static unsigned char at(const Gap *g, size_t i) {
+ return i < g->gap_start ? g->text[i] : g->text[i + (g->gap_end - g->gap_start)];
+}
+
+int main(int argc, char **argv) {
+ if (argc != 5 || (strcmp(argv[1], "copy") && strcmp(argv[1], "draft") && strcmp(argv[1], "create"))) {
+ fputs("bench create|copy|draft DIRECTORY SIZE SAVES\n", stderr);
+ return 2;
+ }
+ const char *mode = argv[1], *path = argv[2];
+ size_t size = strtoull(argv[3], NULL, 10), saves = strtoull(argv[4], NULL, 10);
+ int64_t revision = 1;
+ if (!strcmp(mode, "create")) {
+ unsigned char *body = malloc(size);
+ if (!body || pure_store_init(path) != PURE_OK) return 1;
+ for (size_t i = 0; i < size; ++i) body[i] = (unsigned char)('a' + i % 26);
+ return pure_store_put(path, "doc", 0, (PureSpan){body, size}, &revision) != PURE_OK;
+ }
+ double *save = malloc(saves * sizeof(*save)), *key = malloc(saves * sizeof(*key));
+ if (!save || !key) return 1;
+ size_t cursor = size / 2, length = size;
+ if (!strcmp(mode, "copy")) {
+ PureStoredView view;
+ if (pure_store_get(path, "doc", PURE_OBJECT_LIMIT, &view) != PURE_OK) return 1;
+ Gap g = {malloc(size + 4096), size + 4096, cursor, cursor + 4096};
+ if (!g.text) return 1;
+ memcpy(g.text, view.body.data, cursor);
+ memcpy(g.text + g.gap_end, view.body.data + cursor, size - cursor);
+ pure_store_release(&view);
+ for (size_t i = 0; i < saves; ++i) {
+ double start = now_us();
+ g.text[g.gap_start++] = (unsigned char)('A' + i % 26);
+ ++length;
+ key[i] = now_us() - start;
+ start = now_us();
+ if (pure_store_get(path, "doc", PURE_OBJECT_LIMIT, &view) != PURE_OK) return 1;
+ size_t old = view.body.size, prefix = 0, suffix = 0;
+ while (prefix < old && prefix < length && view.body.data[prefix] == at(&g, prefix)) ++prefix;
+ while (suffix < old - prefix && suffix < length - prefix &&
+ view.body.data[old - suffix - 1] == at(&g, length - suffix - 1)) ++suffix;
+ size_t remove = old - prefix - suffix, insert = length - prefix - suffix;
+ unsigned char *bytes = malloc(insert ? insert : 1);
+ for (size_t k = 0; k < insert; ++k) bytes[k] = at(&g, prefix + k);
+ pure_store_release(&view);
+ PureResult result = pure_store_splice(path, "doc", revision, prefix, remove,
+ (PureSpan){bytes, insert}, PURE_OBJECT_LIMIT, &revision);
+ free(bytes);
+ save[i] = now_us() - start;
+ if (result != PURE_OK) return 1;
+ }
+ free(g.text);
+ } else {
+ PureDraft *draft;
+ if (pure_draft_open(path, "doc", &draft) != PURE_OK) return 1;
+ for (size_t i = 0; i < saves; ++i) {
+ unsigned char byte = (unsigned char)('A' + i % 26);
+ double start = now_us();
+ if (pure_draft_splice(draft, cursor++, 0, (PureSpan){&byte, 1}) != PURE_OK) return 1;
+ key[i] = now_us() - start;
+ start = now_us();
+ if (pure_draft_commit(draft, &revision) != PURE_OK) return 1;
+ save[i] = now_us() - start;
+ }
+ pure_draft_close(draft);
+ }
+ qsort(save, saves, sizeof(*save), compare);
+ qsort(key, saves, sizeof(*key), compare);
+ printf("{\"mode\":\"%s\",\"document_bytes\":%zu,\"saves\":%zu,\"keystroke_median_us\":%.2f,"
+ "\"save_median_ms\":%.3f,\"save_p90_ms\":%.3f,\"peak_rss_kib\":%ld,\"final_revision\":%lld}\n",
+ mode, size, saves, key[saves / 2], save[saves / 2] / 1e3, save[saves * 9 / 10] / 1e3,
+ peak_kib(), (long long)revision);
+ free(save);
+ free(key);
+ return 0;
+}
diff --git a/research/experiments/009-runtime-drafts/run.py b/research/experiments/009-runtime-drafts/run.py
new file mode 100755
index 0000000..08ed55d
--- /dev/null
+++ b/research/experiments/009-runtime-drafts/run.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""Run bench.c in copy and draft modes on a real device, alternating order."""
+import argparse
+import json
+import platform
+import shutil
+import subprocess
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+CONNECTIONS = HERE.parents[2] / 'prototypes' / 'connections'
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--work', type=Path, default=CONNECTIONS / 'build' / 'bench-drafts')
+ parser.add_argument('--size', type=int, default=1048576)
+ parser.add_argument('--saves', type=int, default=24)
+ parser.add_argument('--rounds', type=int, default=3)
+ parser.add_argument('--output', type=Path)
+ args = parser.parse_args()
+ shutil.rmtree(args.work, ignore_errors=True)
+ args.work.mkdir(parents=True)
+ device = subprocess.check_output(['findmnt', '-n', '-T', str(args.work), '-o', 'SOURCE,FSTYPE'], text=True).split()
+ assert device[1] != 'tmpfs', 'fsync is free on tmpfs'
+ binary = args.work / 'bench'
+ subprocess.run(['cc', '-std=c17', '-O2', '-Wall', '-Wextra', '-Werror', f'-I{CONNECTIONS}',
+ str(HERE / 'bench.c'), str(CONNECTIONS / 'store-tree.c'), '-o', str(binary)], check=True)
+ runs = []
+ for round_number in range(args.rounds):
+ for mode in (('copy', 'draft') if round_number % 2 == 0 else ('draft', 'copy')):
+ directory = args.work / f'{mode}-{round_number}'
+ subprocess.run([str(binary), 'create', str(directory), str(args.size), '0'], check=True)
+ line = subprocess.check_output([str(binary), mode, str(directory), str(args.size), str(args.saves)], text=True)
+ runs.append(json.loads(line))
+ print(line.strip(), flush=True)
+ shutil.rmtree(directory)
+ report = {'device': device[0], 'filesystem': device[1], 'machine': platform.machine(),
+ 'kernel': platform.release(), 'store': 'store-tree.c', 'runs': runs}
+ if args.output:
+ args.output.write_text(json.dumps(report, indent=2) + '\n')
+
+
+if __name__ == '__main__':
+ main()