aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments
Commit message (Collapse)AuthorAgeFilesLines
* Measure a link at rest: 132 bytes against a QUIC connection's 38,106 (094)eunhhu2026-09-287-0/+1974
| | | | | | | | | | | | | | | | | M1 of design 011. A Pure node allows 1,000 to 1,000,000 senders; each sends one order and, once told it is stored, rests. At rest a link costs the node 132 bytes (the sender allowed, 73; its number heard, about 60) and no transport state; with a million the node runs in 139 MB. A quinn 0.11 service holding the same exchange's clients connected at rest costs 38,106 bytes a connection: 289 times as much, past the design's 1/100. A link at rest takes its next record in 0.56 ms to stored, one X25519 on the node included; a new QUIC connection takes 1.06 ms and 2.8 times the receiver's CPU, without storing. research/experiments/019-idle-links: links.c, many senders written from serve/link.h's packet format alone (a second implementation), and the quinn service and clients (quic/, built offline); run.py makes both measurements.
* Measure TPC-C again with compact log recordseunhhu2026-09-282-12/+12
|
* Check TPC-C's consistency from one snapshot a side, and measure TPC-C on RAM ↵eunhhu2026-09-282-0/+14
| | | | and an SD card
* Record queries on the bank beside SQLite (091)eunhhu2026-09-281-0/+4
| | | | | | | | | | | | 091 measures four queries on pgbench's tables of 100,000 accounts, the same rows and answers on Pure (registered queries, HTTP and JSON included) and SQLite 3.46.1, through each step: items field by field in leaves, folds and filters a leaf at a time, and one pass answering a fold in a column for every row. Per-branch totals went from 398 ms to 4.9 ms (SQLite 45, 14.3 with a DBA's indexes), a three-table join from 3.3 to 0.48 ms (1.47), and each rich account's rank from 9.2 s to 6.8 ms (2.84 s, 7.6 ms with an index); counting accounts in credit takes 1.05 ms where an index lets SQLite count 0.16 ms. Pure declares no index.
* Run pgbench's transaction on a bank of 100,000 accounts, beside SQLite (090)eunhhu2026-09-271-0/+15
| | | | | | | | | | | | | | | | | benchmarks/bank/bank.aup keeps accounts, tellers and branches as keyed lists and a history list; a transfer changes a row of each and appends a history row in one event, as pgbench's TPC-B-like transaction does in SQL. run.py makes a scale's rows and drives transfers through the load generator's new transfer kind; sqlite-bank.c runs the same tables and SQL on SQLite 3.46.1, a connection a thread, WAL. With lists as trees, transfers run at 30.5k a second on 1,000 accounts and 24.5k on 100,000 (RAM store, one connection thread, HTTP and JSON included), where they ran at 3.2k on 30,000 before. Durable on the SD card, Pure makes 1,339 a second and SQLite with synchronous=FULL 84 to 153. SQLite in process with no HTTP makes about 40k in RAM: recorded as it is, with the write amplification of 100,000-row tables on the card and ad hoc queries as what comes next.
* Record the messenger between two nodes and its latency (089)eunhhu2026-09-272-0/+128
| | | | | | | | | | | | | | The roadmap's E2EE messenger made of the runtime's general parts, with no messenger code in it: sealed, authenticated sessions between nodes (086, 088), Vega on each node's display as a surface (design 010), the app in Vega's examples. Vega's check runs two nodes through a recording relay: a line typed on one display is drawn on the other, frames match the vega command's, and the relay sees no text. A line takes 0.79 ms at p50 and 1.1 ms at p99 from one node's program to the other node's views with stores in RAM, both commits, the HTTP delivery and the seal included (sealing adds nothing measurable); on the Pi's SD card 3.5 ms at p50, the long tail being the card's syncs.
* Seal in one session a wire, authenticated by the sender's key (088)eunhhu2026-09-272-6/+17
| | | | | | | | | | | | | | | | | | | | | | A sealed request cost three X25519 operations (about 0.5 ms), so sealed sends drained at half the plain rate. A wire now keeps one HPKE context, encapsulated at its first seal, and seals each request as the context's next number (a failed seal or a restart starts a new session); the header carries the session's encapsulated key and the number, so a receiver opens a session once per connection thread (16 remembered, the least used replaced) and each seal by its number, also after it restarts, unasked. A sender with a key of its own seals in HPKE auth mode and names that key in the header; a receiver started with --sender NAME=KEY takes deliveries only from the senders it names, each sealed with its key (403 sender otherwise). The body is now the ciphertext and its tag. Sealed sends, sender authenticated, drain at 461-464k a second against 462-470k plain and 207-234k sealed a request at a time (Raspberry Pi 5). The check adds: a wire's requests share one session numbered from 0; a changed number or sender key does not open; a receiver restarted naming its sender opens the same session again; another node claiming that sender's name with its own key is refused and applies nothing.
* Record the small reads and idle memory work, next to axum and drogon (087)eunhhu2026-09-273-0/+147
| | | | | | | | | | | | What the app model, snapshot reads and delivery had cost small reads (126.7k -> 116.1k GETs a second) and idle memory (2.0 -> 4.5 MiB), how the sampler found it, and the four steps and the crypto split that took it back: 139.9k and 2.3 MiB. Measured again today beside axum 0.8.9 and drogon 1.9.0 on the same Pi: a one-thread pure-serve answers small GETs at 139k/s (2.2x axum and drogon a CPU second), 1 MiB GETs at 2,362/s (axum 1,304, drogon 754), durable app writes at 21.3k/s (both about 200), with the least idle memory of the three.
* Map OpenSSL only in a server that uses iteunhhu2026-09-271-2/+3
| | | | | | | | | | | | | | | | | | Linking OpenSSL cost every pure-serve 2.3 MiB of resident memory, 0.66 MiB of it the process's own (relocated data), though most servers never reach an https receiver or seal a send. The code that uses OpenSSL (TLS to https receivers, sealing) is now built as pure-crypto.so beside pure-serve and exports one table; pure-serve maps it the first time a wire to an https URL opens or a key is read or a seal wired (serve/crypto.c), and a missing library is reported at startup. What needs no OpenSSL, a key's hex and the suite this CPU seals with, stays in pure-serve. An idle server: RSS 4,496 -> 2,320 kB, PSS 1,639 -> 881 kB, private dirty 1,296 -> 864 kB. A static OpenSSL was measured too and is worse (RSS 3.8 MiB, but 2.3 MiB private: all of its tables relocated). The checks, https webhooks and sealed sends included, pass unchanged.
* Seal delivered sends to the receiver's key (086)eunhhu2026-09-272-0/+140
| | | | | | | | | | | | | | | | | | | | --seal OUTPUT=KEY seals each request's body whole to the receiving node's public key with HPKE (RFC 9180, base mode: X25519, HKDF-SHA256, AES-128-GCM where the CPU has AES, else ChaCha20-Poly1305), bound to the request's source, first number and count; X-Pure-Seal names the suite. A receiver's --key is a raw 32-byte file, made 0600 when missing, held in OpenSSL's secure heap; it opens the body where it lies (400 seal when it does not open) and zeroes the plain text when the request's ticket goes. --sealed-only refuses unsealed deliveries (403 unsealed). The check puts a recording relay between two apps: every order applies once, no body shows an item or a field name, a flipped bit is refused, a replayed request applies nothing twice, and a seal to another key does not open. Its hosts choose their own ports (--port 0). Sealed sends drain at 233-235k a second against 469-470k plain (Raspberry Pi 5): three X25519 operations a request, 164 us each. The plain request path measured the same as before.
* Give a compacted file the zeros of its whole cycle (085)eunhhu2026-09-275-3/+149
| | | | | | | | | | | | | | | | Commits grew the tree store's tail 256 KiB at a time, each growth a sync that also committed the ext4 journal for the new size and blocks. A file installed by compaction now carries written zeros up to where the next compaction will come (4 x live + slack, plus a commit's margin), capped at the size of the file it replaces, so a store never takes more space than before and shrinks when its live data does. The cycle's commits then only overwrite blocks the file has. Store commits: 317.5 -> 385.5 a second, ext4 journal commits 109 -> 45 for 4,000 commits; durable app posts +5.3%, their p99 the same within this SD card's noise. The new cycles test keeps sizes still between compactions once the live data is whole, and shows the file shrinking with it.
* Measure what moving saves off the owner would buy (084)eunhhu2026-09-272-0/+116
| | | | | | | | | | | | Nothing a thread can give: with writes saturated the device's one line of syncs is the bottleneck, so zeroing the tail or compacting on another thread only queues that device work behind the commits, and the owner's idle gap is shorter than one journal commit. Built without compaction the store is no faster; without compaction and without tail growth durable posts gain 1.4% and the p99 drops 24% (17.4 to 13.3 ms). New blocks are the tail latency, so the way to it is a store that rewrites its space in place, a ring cleaned forward instead of compacted into new files.
* Measure whether sharding owners by key would add writes (084)eunhhu2026-09-273-0/+380
| | | | | | | | | | | It would not, on this device. One file's syncs or four at once, the SD card completes the same 510-690 commits per second, the latency growing with the number of files. The scoreboard app with two owners, each with a store of its own, took 23% fewer durable posts than one owner (median 16,230 against 20,974 a second) with 2-3x the p99: syncs queue, and each carries half the events. One owner's group commit stays; cores serve reads, and writes grow by making the sync itself cheaper.
* Lay Pure out as a core with the parts selected around iteunhhu2026-09-273-12/+26
| | | | | | | | | | | | | | | | | | | | | The app model, the tree store and the HTTP host lived in prototypes/connections among the earlier document runtime, the store was known only by its format's name, and Vega compiled six Pure sources by path. Now, as the README's principle has it: core/ runtime, flow and the app model (module*.c), with what the core asks of storage (state.h, store.h) store/ the tree store (PURETRE3, PUREREC1) serve/ pure-serve: HTTP host, outbound delivery, JSON in place cli/ pure-run, pure-edit-module A top-level make builds libpure.a (app model and store, for hosts to link) and the hosts; make check runs the store's and the app model's tests. The earlier runtime still builds from prototypes/connections against core/ and store/ (check, check-tree and check-core pass). Design 009 records the rule: parts by role behind libraries; git repositories split by coupling, so the store stays here while its contract moves with the core, and Vega stays apart.
* Write each commit's head just before its bytes: one range per sync (081)eunhhu2026-09-263-0/+324
| | | | | | | | | | | | | | | | | | | | | | | A tree store commit wrote its bytes over the written zeros of the tail and its head into a slot at the start of the file: two ranges, and on the Pi's SD card a sync of two ranges costs about twice one (2.4 ms against 1.2 ms). A commit now writes a 128-byte record (its head) at the published end and its bytes right after it, then syncs once. Every 16 commits the head also goes into the older slot. The newest commit is found from the newer slot's head, or from the newest head this process has seen in the file while its record is still there, along the records whose bytes are whole; a torn record or bytes end the walk at the commit before, which the next writer overwrites. Slots of this format are PURETRE3; files of the earlier one are read and continued. Store commits went from 220-233/s to 371-396/s (p50 2.5 to 1.3 ms), and the scoreboard's durable posts from 13-16k/s to 21-26k/s with 64 connections and from 2.2-2.4k/s to 2.9-3.9k/s with 8, measured alternately with the previous build. The torn head check now tears a slot (the walk goes on from the other) and the newest record (a new process sees the commit before); a new check walks 42 commits from the slots, in a fresh process too, and stops at a damaged record.
* Compare the HTTP host with drogon and by CPU timeeunhhu2026-09-262-0/+127
| | | | | | | | | | | | | | | | | A drogon 1.9 service with the axum baseline's contract joins the benchmark (memory, fdatasync and crash-safe modes, three IO threads), built from Debian's packages unpacked under .deps without installing them. drogon listens once per event loop, so the service picks a free port itself: with port 0 every loop got a port of its own and the load reached one. The load generator has one core and saturates near 150k answers a second, so the harness now also reports answers per second of server CPU time (all threads). Over three rotated rounds: small GETs 151.6k/s on three shards (drogon 145.3k, axum 120.9k) and 128k per CPU-second on one thread (drogon 65k, axum 62k); 1 MiB GETs 2,794/s (axum 1,372, drogon 708); crash-safe PUTs 5.1-5.8 times axum and 13-30 times drogon; peak RSS 5.0 MiB against 14.4 and 76.5 MiB. --workloads is now a regular expression.
* Make a tree store commit durable with one synceunhhu2026-09-264-0/+328
| | | | | | | | | | | | | | | | | | | | | | A commit used to sync its nodes, then write the head and sync again: the order of writes was forced by syncs. Now the head carries where the commit's bytes start and their digest, and one fdatasync makes the bytes and the head durable together. A crash during it can leave a head whose bytes did not all reach the device; its digest then fails and the older slot stands, which is safe because a writer only builds on a durable head. Commits overwrite written zeros kept after the published end, so the sync changes neither the size nor the block map and needs no journal commit. On the SD card this is 2.4 ms per commit against 9.1 ms for two syncs; the store commits 289-304 times a second instead of 94-109, and pure-serve answers 2,012-2,611 durable PUTs a second against 583 for crash-safe axum. A process remembers the heads it published or checked, so it reads a commit's bytes at most once and never its own. Its readers pass over the head it is still making durable and so never answer with a commit a crash could take back. Head slots get a block each and the format becomes PURETRE2; a handle's refresh reads the slots through its mapping.
* Let the runtime own working versions as draftseunhhu2026-09-253-0/+234
| | | | | | | | | | | | | | | | | | state.h gives any app a runtime-owned working version of a stored value on the tree store. A draft shares unchanged extents with the stored version, edits in memory by path copying, lends ordered extents without copying, and commits only its new nodes when the key is still at its base revision. A failed commit changes neither the draft nor the store; a draft whose file was replaced by compaction copies its stored extents on commit. Unreachable arena nodes are reclaimed once they outweigh max(reachable, 32 KiB). Typing into the middle of a 1 MiB document and saving each character peaked at about 1,660 KiB RSS with a draft against about 4,480 KiB for an app copy compared with storage on save; saves were only 1-2 ms faster. This corrects 073, which had attributed the TUI's remaining 8 ms to that comparison. Drafts are not yet charged to the runtime budget or visible to .pure definitions.
* Add selected single-file persistent tree storeeunhhu2026-09-255-0/+912
| | | | | | | | | | | | | | | | store-tree.c keeps every document in one append-only file as a persistent B-tree of byte extents, with the key catalog in the same structure. A splice writes only the changed leaf and path; unchanged extents are shared between versions, readers take no lock, and two alternating head slots publish a commit after its nodes are durable. Compaction rewrites reachable nodes. On the recorded ext4 host a durable 1-byte middle insert into a 1 MiB document took 8.58 ms against 78.51 ms for the default store, and the real TUI's Ctrl+S after typing took 16.60 ms against 84.59 ms. check-tree runs the store's crash/oracle tests and the unchanged runtime, splice, async and real-PTY editor checks on it. It stays opt-in: linked code is 12.7 KB larger and contiguous multi-leaf reads copy.
* Verify paged document app under 4 KiB AArch64 kerneleunhhu2026-09-242-0/+196
|
* Establish native micro runtime checkpointeunhhu2026-09-2492-0/+88792