diff options
Diffstat (limited to 'research')
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() |
