aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* Record the messenger between two nodes and its latency (089)eunhhu2026-09-275-4/+215
| | | | | | | | | | | | | | 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.
* Run a surface for people inside the node (design 010)eunhhu2026-09-278-14/+449
| | | | | | | | | | | | | | | | | | | | | | | pure-serve --surface LIBRARY [-- ARGUMENTS] maps a library and runs its pure_surface_run in a thread of its own (serve/surface.h). The surface reads the app as of the last save where it is, a published snapshot in a reader app, as a connection thread reads views; an eventfd tells it of each new snapshot; and its events (a key pressed, text typed) are requests without a host in the owner's queue, applied and saved with the round, their results handed back to the surface's waiter. So one node serves programs over HTTP and people on its display with one owner of its states, and nothing between the states and the screen. pure-serve exports its pure_* functions for the surface to call, one copy of the app model in the process; the table carries sizeof(PureApp) and sizeof(PureModule) so a surface built for another layout refuses. The check builds a probe surface that types, presses and reads: its events show in HTTP views, an HTTP input shows in it, wrong inputs are refused, it stops with the server, and a module without states cannot have one. Without a surface the request path measured the same (small GETs 140.9k vs 139.9k, app totals 126.3k vs 126.3k over eight rounds); the exported symbols cost 11 KB of the file and 24 KiB idle.
* Seal in one session a wire, authenticated by the sender's key (088)eunhhu2026-09-2713-155/+502
| | | | | | | | | | | | | | | | | | | | | | 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-276-3/+270
| | | | | | | | | | | | 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-2711-112/+276
| | | | | | | | | | | | | | | | | | 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.
* Stop reading a connection at a short readeunhhu2026-09-271-3/+17
| | | | | | | | | | | | | | | | | | After answering, a connection went back to recv for its next request and got EAGAIN nearly every time: a failed syscall a request. A recv that returns less than it asked for has emptied the socket, and the edge-triggered epoll reports the next bytes when they come (epoll(7); as nginx does), so the connection now waits for that; a peer that hung up is read to its end as before. Small keep-alive GETs, 64 connections, five rounds in rotation (Raspberry Pi 5): 131.9k/s before this step, 139.9k after. With the three steps before it (a burst's keys in one look, the clock once a round, the head without printf): 116.1k -> 139.9k/s, p99 672 -> 512 us, above the 127.0k of 2026-09-26's build that the app model had since slowed. App reads: the total 111.5k -> 125.4k/s, a 20-row page 86.2k -> 94.3k; 1 MiB reads and durable posts unchanged.
* Write an answer's head without printfeunhhu2026-09-271-9/+45
| | | | | | | | | Three snprintf calls a response (the head, its ETag and a delivery's batch size) spent a sixth of a small read's user time in printf's format scanning. The head is now written part by part, the same bytes as before, clipped at the end of its buffer where snprintf's would-be length could have sent bytes past it.
* Read the clock once a roundeunhhu2026-09-271-8/+11
| | | | | | | | | | A connection's deadline was counted from a fresh clock_gettime on every read and write, three times a request, and every event took the wall clock again: 5.7% of a small read's user time. A connection thread now reads the monotonic clock once after each epoll_wait and deadlines count from that (they are 10 s by default and swept every second), and a round's events share the time it began.
* Find a burst's keys in one lookeunhhu2026-09-274-11/+62
| | | | | | | | | | | | | | | A message for a document scanned the burst's pending keys with strcmp to see whether its key was there, and the host scanned the burst's earlier requests the same way before each one: both quadratic in the burst (64 concurrent reads, about 31 compares each, twice). The app now indexes its pending documents by key (open addressing over an FNV-1a hash, 1 KiB), answers pure_app_pending in one look, and the host asks it; whether the current event's document is pending is a flag instead of a scan by pointer. Bursts split where they did, except that on an owner, which applies a round's reads first, an edit of a key read in the same round now splits the burst at the host rather than the app saving on its own. A unit test checks the index at 200 keys.
* Add a sampling profiler for machines without perfeunhhu2026-09-273-0/+381
| | | | | | | | | | | | benchmarks/sample.c runs a command and samples where its threads spend user time (the task clock every 50 us, one perf_event_open event a CPU, since the kernel maps no ring for an inherited per-task event), then names the samples from the ELF symbol tables of the files the command had mapped: a stripped library from its debug file by build id. On arm64 it also takes the link register, so time in a library's leaf (memcpy, strcmp) is shown again by its caller. The command's user and system time head the report.
* Seal delivered sends to the receiver's key (086)eunhhu2026-09-2712-16/+676
| | | | | | | | | | | | | | | | | | | | --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.
* Send a delivery's request in one pieceeunhhu2026-09-271-7/+24
| | | | | | | | | | | | A batch went out as a write for its head, then a bracket or comma and the send for each of its sends: two writes a send. Now the head and the whole body are built in one buffer and written once. A receiver stopped while 20,000 sends piled up drained them at 91k sends a second before and at 470k after (three rounds each, Raspberry Pi 5). Writing the head apart from the body measured the same (468k), so the gain is the body's; one buffer is kept as the simpler of the two, and a lone send now leaves in one segment instead of two.
* Bring the checkpoint's open items up to 084 and 085eunhhu2026-09-271-2/+6
|
* Give a compacted file the zeros of its whole cycle (085)eunhhu2026-09-278-10/+274
| | | | | | | | | | | | | | | | 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-273-2/+146
| | | | | | | | | | | | 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-274-0/+427
| | | | | | | | | | | 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.
* Check pure-serve in Pure, not in aupeunhhu2026-09-2710-9/+1255
| | | | | | | | | | The HTTP host's end-to-end checks (documents against the earlier server, lending, kill -9 durability, JSON, timers, links, webhooks and answers) sat in aup's tests, so a change to the host was verified from the toolchain. They now live in tests/check.py; make check builds their apps with the sibling aup and runs them after the unit tests, which stay beside their parts. Design 009 states the rule for all three repositories.
* Show the new layout costs nothing, and keep code where it waseunhhu2026-09-273-1/+17
| | | | | | | | | | | | The benchmark runs an earlier pure-serve build in turn with the current one (--baseline). Against the build before the move, linking through libpure.a costs nothing: a one-command build with the library's object order has the same size and the same CPU per request. What moved was code placement, so the runtime links last again, as it did; the two orders now differ by less than rounds of one build do (total -0.4%, page +1.2%, mixed -0.1%). The binary is 5,008 B smaller: unused flow.c functions stay in the library, and the linker's A53 erratum padding is gone.
* Lay Pure out as a core with the parts selected around iteunhhu2026-09-2750-79/+222
| | | | | | | | | | | | | | | | | | | | | 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.
* Look for new sends newest first, only as far back as they goeunhhu2026-09-275-13/+45
| | | | | | | | | After each save the owner walked the whole outbox to find the sends not yet queued, so a receiver long down made every save walk the backlog. pure_app_outbox_newest visits saved sends newest first, and the owner stops once every wired output reaches the number it queued last, then queues what it found in order.
* Make an API's answers the app's events (--reply OUTPUT=INPUT)eunhhu2026-09-276-35/+281
| | | | | | | | | | | | | | | | | | | | | The final answer to each send of a wired output (a 2xx, or a 4xx but 408 and 429) becomes a message event of the given input: fields from the answer's JSON object by name, other members skipped; a field the answer lacks from the send's field of that name and type, so the request's own id is there; a u32 http_status field is the status. It is applied with the send's number from the wire's source, before the send is trimmed and in the same save, so an answer that comes again after a restart (the API repeating itself for the Idempotency-Key) applies once. Sends that wait for answers go one per request; answers are saved with the next round, or at once when none waits. A server that writes an answer in pieces holds the second until the first is acknowledged, and a delayed acknowledgement stalled every answer 40 ms: the client now acknowledges at once (TCP_QUICKACK) after each read. Against a payments API that answers 200 or 402, with the sender killed (-9) midway, paid and declined orders matched the API's answers one to one.
* Read JSON messages in message-json.c, for requests and answers alikeeunhhu2026-09-274-222/+319
| | | | | | | | | The in-place JSON reader of pure-serve's request bodies moves into its own file with a small interface (pure_json_message, pure_json_fixed, pure_json_space), so the delivery client can read an API's answers with it. It gains a lenient mode for answers: a member that names no field is skipped, value and all. Requests read as before.
* Deliver to https receivers, verifying their certificates (083)eunhhu2026-09-277-33/+107
| | | | | | | | | | A wire to an https:// URL speaks TLS 1.2 or later through OpenSSL: the receiver's certificate must chain to the system's CAs (or to the file --send-ca names) and name its host (SNI and SSL_set1_host, or the IP address). An untrusted receiver gets nothing: the send stays in the outbox, the wire logs it and tries again every 10 s. pure-serve now links libssl and libcrypto.
* Deliver an app's saved sends over HTTP, in order and in batches (083)eunhhu2026-09-2712-48/+852
| | | | | | | | | | | | | | | | | | | | pure-serve --send OUTPUT=http://host:port/path wires an output: after every save the owner queues the new sends as JSON (fields and groups) and a thread per output posts them in order with X-Pure-Source, X-Pure-Sequence and Idempotency-Key. A 2xx, or a Pure receiver's refusal, delivers; the owner then trims. 408, 429, 5xx and no answer are sent again after a pause growing to 10 s, other answers every 10 s: nothing is dropped. A Pure receiver applies a delivered message with pure_app_deliver (now taking placed fields) and advertises X-Pure-Batch: 256; the sender then posts up to that many numbers in a row as one JSON array (X-Pure-Count), applied in order in one burst with one save. Delivery kept up with orders once batched (391 deliveries/s one at a time; a backlog of 20,000 went at 60k/s). With four clients ordering, killing the receiver and then the sender (-9) left the receiver's count and total equal to what the sender saved, five times and under ASan.
* Send messages as part of events and apply delivered ones once (078)eunhhu2026-09-278-18/+742
| | | | | | | | | | | | | | | | | | | | | | | An output (input kind 5) is a message the app sends; SEND (72) takes an output's field values in order in a handler, lays them out as a message's payload and keeps it, numbered per output from 1, with the event's changes: a failed event sent nothing, and a failed save takes back every send since the last save with its numbers. A host sees only saved sends (pure_app_outbox) and trims what its receiver has saved (pure_app_trim, gone with the next save). pure_app_deliver applies a message from a named source once: a number heard before succeeds without applying, a refused message counts as heard, a failed save forgets what it heard. Sends, numbers and heard numbers are the app's links state, stored whole in one runtime document (PLK1) with the states, in the same commit. Outputs follow their names across module changes; a module that drops or retypes an output with sends left is refused, and a dropped output's number is kept for when it returns. Killing the process at every commit stage leaves an event and its send, or a delivery and its effect, together or neither. pure-serve answers a post to an output or a timer with 404 no_input.
* Choose the connection threads with --threads autoeunhhu2026-09-272-1/+15
| | | | | | | | One connection thread for each CPU the process may run on (its affinity, or the online CPUs), less the one the owner of an app with states takes: three CPUs give two connection threads and the owner. The default stays one thread.
* Gather after a save until a just-answered client has come backeunhhu2026-09-272-8/+21
| | | | | | | | | | | | | After a round that saved, the owner waited for 100 us without a new request (1 ms at most) before the next round. Requests that queued up during the save made that quiet start at once, so the clients just answered missed the next round; the clients then settled into two halves taking turns, each save carrying half of them. It showed with two connection threads: 34 events a round and 15.8-16.0k posts/s with 64 connections, against about 61 and 22-24k/s with one or three. The quiet now counts only once a request has come after the answers were handed back: two threads carry 61.6 events a round, 22.9-23.9k/s.
* Record the bound of taking tail growth and compaction off commits (081)eunhhu2026-09-271-3/+5
| | | | | | | Measured with a build that grows its tail by 64 MiB once and never compacts: durable posts +5-8% (64 connections), p99 about halved; the slow syncs that remain come from the device.
* Read JSON bodies in place, groups included (082)eunhhu2026-09-279-79/+260
| | | | | | | | | | | | | | | | | | | | | pure-serve no longer copies a JSON body's strings into the message's payload. The ingress is the prefix, a table of each field's start and size, and the body as it came; the connection thread scans the body once, puts numbers into the prefix and records where each string is. Only a string with escapes is unescaped, over itself (it never grows). A group of fields comes as an object inside the body and, for a fixed field, as its path in the query (?to.zip=7). A group where a field belongs, a value where a group does, an array or true/false/null is json_type; an unknown member, inside a group too, json_field. The string scan and UTF-8 checks now pass over eight bytes at a time while none needs a look (pure_utf8_step, so every text check gains). On the Pi, posting {"name":"k","text":"x..."} to a stateless app went from 652-655 to 3,737-3,953 requests/s with 256 KiB of text (server CPU 1.52 to 0.25 ms a request, peak RSS 5.2 to 3.2-3.4 MiB) and from 29.1k to 76.3-76.7k/s with 4 KiB; the load generator gains a json method and benchmarks/http/echo.aup is the app.
* Name message fields by paths and take variable fields where they areeunhhu2026-09-274-25/+110
| | | | | | | | | | | | | | | A message field's name may now be a path of identifiers joined by dots (user.name, a field inside the group user); a module that names both a field and a group (user and user.name) is refused, as are empty, leading, trailing or digit-first segments. A module may have 64 message fields instead of 32 (PureModule 11.9 to 13.2 KiB). pure_app_message_fields takes each variable field's start and size in the payload, so a host that parsed a request in place passes the fields' bytes where they are instead of laying them out after the prefix. The runtime checks them as before: a field past the payload fails with limit, a text that is not UTF-8 with encoding.
* Write each commit's head just before its bytes: one range per sync (081)eunhhu2026-09-268-64/+647
| | | | | | | | | | | | | | | | | | | | | | | 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.
* Show views from the last save's snapshot on connection threads (080)eunhhu2026-09-266-36/+253
| | | | | | | | | | | | | | | | | | | | The owner publishes a snapshot of the saved states after every save, before it hands back the answers; connection threads take the newest one each round and show views from it with a reader app of their own. A read made after a write's answer sees the write, nothing unsaved or undone is ever shown, and reads no longer wait while the owner applies events or syncs. Apps with states always get an owner thread, even with one connection thread. A round's inputs go to the owner first; its views are shown and then answered together (answering each one as it was shown made one thread's 20-row page 8% slower). On the Pi 5, with 8 connections posting durable scores and 56 reading the total, reads went from 10-16k/s at p99 13-29 ms to 107-149k/s at p99 0.6-0.7 ms, and posts stayed in range; axum and drogon, which sync under their lock, read 1.1-1.3k/s at p99 111-156 ms. A snapshot of a changed 1,024-item list costs 1.2 us (65,536 items: 87 us) against a 3-4 ms save.
* Measure reads made while durable writes go oneunhhu2026-09-262-11/+40
| | | | | | | | The app suite's last workload, mixed, runs two load generators at once: 8 connections post durable scores while 56 read the total. It shows whether a server's reads wait while its writes sync. It is reported as two rows, posts and reads; answers per CPU second count both.
* Snapshot an app's saved states for readers on other threadseunhhu2026-09-269-20/+494
| | | | | | | | | | | | | | | | | | | | | | Right after a save, pure_app_snapshot makes a read-only snapshot of the saved states: numbers are copied, text states are frozen drafts that share the store mapping (pure_draft_freeze, no copy), and lists are copies of their item pointers that share the items. An unchanged list shares the last snapshot's copy. pure_app_read binds a reader app on another thread to a snapshot so the existing view functions read it. A shared item never changes again: a list stamps each item with its share count, copies a shared item it changes, and retires shared items it lets go instead of freeing them. Snapshots form a chain that goes oldest first, and items retired while a snapshot was the newest go with it. References are atomic and may be released on any thread. test-module checks that each snapshot equals the app, that kept snapshots stay as made through changes, removals, a module switch, the app stopping and the store closing, that a failed save's snapshot equals the one before, and that two threads read the newest snapshots while the app goes on (also under ASan, UBSan and LeakSanitizer).
* Answer a round's reads before its writes on the ownereunhhu2026-09-261-10/+26
| | | | | | | | | | | | | | With durable writes going on, a read waited for the save of the burst it landed in. A round's requests come from different connections, one request each at a time, so nothing orders them: the owner now applies a round's reads first and answers them at once, seeing only saved states, then its writes in bursts. 8 writers and 56 readers of the scoreboard, owner mode: reads 7,100-7,700 to 16,100-16,600 a second, their p99 55 to 13 ms, writes 1,000-1,100 to 2,300 a second. A single thread keeps the order: it waits out each save itself, and reads answered first come back first and split its next writes over more saves (the owner gathers after a save instead); there reads are 12,800-13,400 a second as before.
* Lend a text state's stored extents for its view (079)eunhhu2026-09-267-27/+113
| | | | | | | | | | | | A view that shows a text state as it is (pure_module_text_state) now goes out as the state's stored extents, as a document read does: the answer holds the store mapping, charged to the budget, until it is sent and its ticket released; a state with edits not yet stored is copied as before. Documents and views share one loan path, and the extent array grows as needed. A 1 MiB note read by 8 connections: 776-817 answers a second copied, 1,875-1,901 lent, p50 9.2 to 3.5 ms, peak RSS 10.3 to 5.7 MiB. The load generator learns GET /views/page.
* Design connections between apps (078), not yet builteunhhu2026-09-261-0/+80
| | | | | | | | | | A sent message belongs to its event's transaction in a runtime-owned outbox; hosts deliver committed messages in order, and a receiver keeps the last sequence per source in the same commit, so the effect is exactly once. The note records the design and its order of work; it waits behind the roadmap's short-term items and goes with outbound calls, which stand on the same outbox.
* Size a module's code and constants to the module; raise their limitseunhhu2026-09-266-45/+93
| | | | | | | | | | | | | A real app (a board, a stopwatch) already used a quarter of the 512 instructions, since a fn is inlined where it is called, and every text it shows is one of 64 constants. The code and the constants now live in one block sized to the module, which load takes and pure_module_release gives back; the limits rise to 65,535 instructions and 4,096 constants of up to 1 MiB, while PureModule shrinks from 28,664 to 11,936 bytes. The verifier's branch marks are sized to the block instead of 64 KiB on the stack. pure_module_read reads a module file whole, and every host uses it instead of a 128 KiB buffer.
* State the current app model in the checkpoint and READMEeunhhu2026-09-262-1/+27
| | | | | | | | | | The checkpoint's table still said the document and scalar apps were not one authorable model, and that equivalent-server latency was unproven. A new section records what has been checked since: one app model (aup to module format v5), persistence, Vega's surfaces and hot swap, apps as HTTP services, and the measurements against axum and drogon, with what remains. The README opens with the same in brief.
* Tick a served app's timers and give its events the clockeunhhu2026-09-262-9/+64
| | | | | | | | | | pure-serve sets the time an event sees to the wall clock when the event is applied, and the thread that runs an app with states (the connection thread alone, or the owner) ticks its timers on time: epoll waits no longer than the next tick, the owner's condition wait is timed to it, and the ticks due at once share one save, so a restarted server goes on from the stored ticks. The statistics count ticks.
* Add time: timers, now, floor and a floored moduloeunhhu2026-09-265-13/+116
| | | | | | | | | | An input may be a timer (kind 4, its period of 1 ms to a day in place of a key name): pure_app_tick delivers a tick, an event saved like a key's. NOW gives handlers the time the host set in PureApp.now (milliseconds since 1970, UTC), so a script replays any time and views stay functions of state; FLOOR and MOD take times apart, MOD taking the divisor's sign as floor division leaves it, a zero too. pure-run gains time MS and tick NAME.
* Take messages of number and text fields, several variable ones, as JSONeunhhu2026-09-265-51/+375
| | | | | | | | | | | | | | | | | | A message field is now a u32, a number (binary64), bytes or text, and variable fields may be several and anywhere: the payload's prefix holds the fixed fields and each variable field's length but the last one's, which takes the rest; a module whose one bytes field came last is laid out as before. On arrival a length past the payload, a text that is not UTF-8 or a number that is not finite fails the message. pure-serve fills an input's message from a JSON object body (Content-Type: application/json) on the connection thread before the request is ready, so the owner never parses and replies are still borrowed: the ingress is [u32 payload size][payload][the JSON as it came]. A missing, unknown, doubled or mistyped field, or text that is not JSON, answers 400 and the request never runs. Numbers may come in the query too.
* Record the app service against axum and drogon (077)eunhhu2026-09-264-5/+116
| | | | | | | | | | | | The scoreboard, 22 lines of aup served by its views and inputs, against the same service written by hand in axum and drogon on the Pi: durable posts about 76 times as many, reads level with drogon and ahead of axum with the least CPU and memory. The note keeps what the profile showed on the way (the kernel's share of a single thread, integers through printf, an owner that saved too early) and what comes next. run.py gains pure-serve-2 and waits for a server that prints its port before it listens.
* Serve an app with states from several connection threadseunhhu2026-09-262-101/+340
| | | | | | | | | | | | | | | | | | | States have one owner, so a module with them ran on a single thread, which spent most of its time in the kernel sending and receiving. With --threads N such a module now gets N connection threads and one owner thread: connection threads accept, parse and answer; the owner takes the ready requests of every thread as one round, applies them in bursts with one save each and hands each thread back its applied requests (eventfd), which it completes and answers. A request's route and payload stay held by its thread's runtime core until completion, so an accepted event runs even when its connection closes first. After a burst that saved, the owner gathers requests until none has come for 100 µs (1 ms at most), so the requests its answers bring back share the next save as they would on one thread: 30 events per save became 50 and the median latency of a durable post fell from 5.4 to 3.5 ms. A page of 20 rows went from 87,600 to 141,900 answers a second on the Pi.
* Build a table's JSON page with fewer appendseunhhu2026-09-261-23/+27
| | | | | | | Each column's key is made once per request and a page's buffer is taken at once, instead of three small appends per cell. A POST with no payload (a key press) no longer copies nothing to a missing ingress.
* Count a store mapping's references atomicallyeunhhu2026-09-261-3/+5
| | | | | | | A read's lent extents hold the store mapping until its answer is sent; with an owner thread running the app and connection threads answering, the last reference may drop on a thread other than the one that took it.
* Format integers without printfeunhhu2026-09-261-1/+11
| | | | | | | | | pure_format_number, behind every number a text shows (interpolation, the views pure-serve answers, pure-run), printed integers with snprintf("%.0f"): hundreds of nanoseconds each on the Pi, most of a page of 20 rows. Integers below 1e15 now take their digits directly (21 ns), identical to printf on 2.6 million random integers.
* Benchmark an app served by its views and inputseunhhu2026-09-266-11/+311
| | | | | | | | | | run.py --suite app serves the scoreboard of scores.aup (22 lines of aup) with pure-serve against the same scoreboard written by hand in axum and drogon: POST /inputs/score?points=N with a name as the body, GET /views/total and a page of 20 rows as JSON. Durable, the baselines append each score to a log and fdatasync it before answering (replayed at start); the load generator learns the three requests.
* Serve an app's views and inputs over HTTPeunhhu2026-09-262-52/+369
| | | | | | | | | | | | | | | | | pure-serve answers GET /views/NAME with a view's value (a number or text as text/plain, a table as JSON rows, a page of them with ?from=&count=, each cell computed only for the rows asked) and POST /inputs/NAME with an event: a text input's text is the body, a key input is pressed, a message takes its u32 fields from the query and its bytes field from the body and answers its handler's reply. A failed event answers with its fault (404 not_found, 409 exists, 422 the others). No routes are written: the app's views and inputs are its interface. Documents stay served when the module has show and edit. The route of a request is kept per work slot, so an accepted event runs even if its connection closes before the burst; views and app events join any burst, whose one save comes before its answers.
* Add keyed lists and text comparisoneunhhu2026-09-267-21/+424
| | | | | | | | | | | | | | | | | | | | | A list state may name a key field of its record (the flags word of its entry: field + 1). A keyed list keeps its items in key order, numbers by value and texts by their bytes, each key once: LIST_APPEND places an item by halving and fails with the new fault exists (PURE_CONFLICT) when the key is there; LIST_FIND gives the index of a key's item and LIST_HAS whether it is there. The verifier refuses inserting by position and changing a key. Maps, sets and sorted maps are one list with an order rule, so tables, filters, folds, indices, storage and replacement stay as they are. A module that opens over a list not in its key order (one that was not keyed, keyed by another field, or stored so) puts it in order, moves its indices with their items and stores it again; two items with one key refuse the module rather than drop one. TEXT_COMPARE compares two texts by their bytes under one of the six relations.