aboutsummaryrefslogtreecommitdiffstats
path: root/serve/link.c
Commit message (Collapse)AuthorAgeFilesLines
* Wake a link's thread only when it sleeps, onceeunhhu2026-09-281-3/+13
| | | | | | | | | | | | | | | Every queued record wrote the link's eventfd, a system call each, and woke the thread to send a packet of a few records. A record queued now wakes the thread only when it sleeps and nobody woke it yet; one queued while it sends marks the link, and the thread looks again before it sleeps. With records queued one at a time as fast as a thread can, 200,000 small ones went at 400k a second, and now at 730k; queuing one costs 0.43 us, not 1.0. research/experiments/019-idle-links/transport.c measures the transport alone: a link and a listener in one process, no app, no store. A backlog of 200,000 records of 61 bytes drains at 3 to 4 million a second, 0.24 us a record on each side.
* Drain a link's backlog without scanning it: 370k to 990k records a secondeunhhu2026-09-281-105/+218
| | | | | | | | | | | | | | | | | | | | | | A backlog made the link slow in its square: the sender looked at every queued record's pieces twice a turn to know whether any waited to be stored, and the receiver moved all its held records down each time it let the first one go. - The sender counts the pieces received and not stored, and keeps where the unsent ones begin; every change of a piece's state goes through one place that keeps both. - The receiver keeps its held records from a head index. One in order goes last, and one out of order finds its place by halving. The record's bytes and its pieces' marks are one allocation. - Packets go in bursts with UDP GSO, 54 at a time: 0.45 us a packet on this Pi, against 2.4 us for a send each. A full packet is padded to a whole datagram (PADDING frames, as QUIC does). Datagrams are taken 64 at a time (recvmmsg). Twenty thousand saved sends of 61 bytes drained at 370k a second; now at 990k. Per record, the sender's link thread took 1.1 us and now takes 0.5; the receiver's took 2.4 and now takes 0.5.
* Name the link threads, so their CPU is seen apart (pure-link, pure-listen)eunhhu2026-09-281-0/+2
|
* Remember each sender's secret beside the node's key: no X25519 at restarteunhhu2026-09-281-3/+67
| | | | | | | | | | | | | | A listener lets a quiet sender's secret go with its active state, so a node started again made one X25519 for every sender that came back (169 us on this Pi, most of a wake's CPU): asymmetric work in proportion to the links, which design 011 counts as a reason to design again. The secret is now remembered in a file beside the key (KEY.links, 0600, begun again for another node's key): a place of 64 bytes a sender in the order allowed, mapped, so a sender at rest takes no memory for it. A place is the first 28 bytes of the sender's key, a check (FNV-1a of the key and the secret) and the secret, written secret first and key last; a place whose check fails is made again. The UDP link checks see a receiver started again make no X25519.
* Keep a run's keys as bytes: one AES-GCM context serves the listenereunhhu2026-09-281-42/+53
| | | | | | | | | | | | Each run a listener knew held two OpenSSL contexts. With 16 KiB pages, what those allocations left behind when idle senders went kept 42 MB of free heap resident after 10,000 senders had come and gone (in use: 0.6 MB). A run now keeps its keys (16 bytes each way and the nonce bases), and the listener seals and opens with one context, giving it each packet's key: about 130 ns a packet on this Pi. Two runs a sender, not four; a sender's address as an IPv6 one (an IPv4 one fits). What idle senders held goes back to the system once 1,024 have gone, or 10 s after any went (malloc_trim). HKDF is fetched once, not at every derivation.
* Let a link at rest cost the listener only its allowingeunhhu2026-09-281-158/+358
| | | | | | | | | | | | | | | | A sender allowed is now a peer of 48 bytes (its key, its key's id, its sources), found through an index by key id; ids are 32 bits, so keys that share one are each tried. What a sending peer needs besides (the secret its key shares with the node's, its runs' keys and addresses, its streams) is made at its first packet and let go once it has been quiet for the idle period (--link-idle-ms, 30 s at first) with nothing of it waiting: the next packet makes it again, one X25519, no handshake. The listener hands over and answers only the streams and runs that changed, not every one at each turn. --senders FILE allows senders a line each. The UDP link checks take their sender from such a file among a thousand others, let it go after 400 ms of quiet, and see an order after a longer pause arrive all the same.
* Link two nodes without a connection over UDP (093, design 011's first stage)eunhhu2026-09-281-0/+1214
pure-serve --send OUTPUT=pure://host:port/INPUT sends an output's saved sends to another node's --link-port with no handshake: the first packet carries records. Keys come from the two nodes' keys (X25519, HKDF-SHA256, AES-128-GCM), with a random run in each packet's header so no nonce repeats across restarts; nothing is stored for them. What lasts is the apps' own: the sender's outbox and the receiver's heard numbers (pure_app_heard, new). The sender detects losses as RFC 9002 says, with NewReno, and trims a send once the receiver says it stored it; the receiver hands the owner whole records in order, which it applies from the one after the source's last heard number, stopping at a gap, and answers how far it stored with every acknowledgment. Stored bytes are the bytes on the wire: an output and an input of one shape need no JSON. server_links_udp kills the receiver, the sender, then both (-9) while four clients buy: every saved order is applied once, with no handshake (five runs, 10k-11k orders each). server_links_udp_lossy does it again in a namespace of its own with the link's packets delayed, lost (3 %) and reordered (10 %) by netem (three runs, 11k-12k orders).