aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments
Commit message (Collapse)AuthorAgeFilesLines
* Say what a search's floor is with stored lists as trees: 373 ns a node to ↵eunhhu2026-10-053-0/+75
| | | | find a slice against C's 21 (021)
* The same answers on the final build and the same pure-runs on the Pi, WSL2 ↵eunhhu2026-10-044-0/+145
| | | | and a Mac (121); 019 points at 020's view workers
* Measure views off the connection threads: writes' tail as with the CPU ↵eunhhu2026-10-049-3/+71
| | | | shared, two askers 1.95 times one (122)
* Measure computing faster: a search 2.1 times as fast, TPC-C 4.1%, four ↵eunhhu2026-10-0466-0/+399
| | | | fingers found and undone (121)
* The runs of research 122: writes beside views that compute, one asker and ↵eunhhu2026-10-043-0/+283
| | | | two, and a plain view's reads
* Wait for a counter before searching in bfs-run, and the runs of research 121eunhhu2026-10-042-0/+67
|
* Say where a search's time goes and how to make it and long views fast, with ↵eunhhu2026-10-041-0/+83
| | | | criteria (020)
* Wake each thread at most once a push, end streams only of stopped apps, and ↵eunhhu2026-10-043-0/+93
| | | | show a subscriber costs writes nothing (019)
* Say what pushing views costs and how fresh it keeps them (019, research 120)eunhhu2026-10-0451-0/+365
|
* Say what views that compute cost the writes beside them (018 stage 3, ↵eunhhu2026-10-046-0/+299
| | | | research 119)
* Say what computing in one event costs and finds (018 stage 2, research 118)eunhhu2026-10-043-3/+95
|
* Measure 018 stage 2: a graph search in one event, the pairs, and runs to ↵eunhhu2026-10-0434-0/+511
| | | | compare across machines (033)
* Say what a transient state saves a write (018 stage 1, research 117)eunhhu2026-10-043-0/+87
|
* Say what an app's tokens cost (017 3b, research 116)eunhhu2026-10-042-0/+109
| | | | | | | | Requests to an app that checks tokens go 2.92% slower than to an open one on 64 keep-alive connections, 2.2% of it the head the token makes longer (an open app sent the same head is as slow); hashing every request on a new connection costs 0.91%. 031 measures the three in turns.
* Say what apps that change on a running host showed (017 stage 3, research 115)eunhhu2026-10-0443-14/+605
| | | | | | | | | | Changing an app in place keeps a million items' app answering but for 1.12 ms (a host restart: 150 ms; stopping and starting it again from its store, as first built, 150.4 ms), 171.5 ms when its records change shape (332 ms). Another app's p99 stays 0.97 times as it was while one changes every 0.2 s; a host of one app is as it was (TPC-C +0.7%, instructions at most +0.02%); each module change leaves 19 KB. 030 holds the windows of both builds, the isolation, the pairs, the counts and the memory.
* Measure how long an app changing module cannot answer, against a host ↵eunhhu2026-10-046-0/+293
| | | | | | | | | | | | restart (030) An app of a million items (a list grown by doubling) or of a thousand, in a host of apps, its store in RAM: the longest gap between two answered writes while it changes module on SIGHUP (stopped and started again from its store, 71e319b), and while the host is restarted. A thousand items: 1.59 ms against 3.05 (median of 20 and 10). A million: 150.64 ms against 149.73 (10 and 5): both are the store read again (pure_app_start, 142 ms), so changing does not answer sooner than restarting.
* Trade stores with the WSL2 too: its bytes are the Pi's (029, research 114)eunhhu2026-10-044-0/+27
| | | | | | The same events make byte-identical stores on the x86_64 WSL2 (4 KiB pages) as on the Pi and the Mac, and a store made on either of the Pi and the WSL2 opens on the other and goes on the same.
* Run Pure and aup on an x86_64 WSL2 and an M3 Mac against the Pi (029)eunhhu2026-10-0226-0/+3806
| | | | | | | | | | Every pure-run of aup's check (1,053 runs of 667 modules) gives the same output, errors and exit on the Pi, the WSL2 (x86_64, 4 KiB pages, gcc 13) and the Mac (Apple clang 21); the same events make byte-identical stores on the Pi and the Mac, and either opens the other's and goes on the same. Pure's checks pass one at a time in WSL (each_check.py); its wall clock is set back about 2 s every 32 s (clock.py). stores.py streams pure-run's output, whose size grows with the square of the events.
* Measure wires on the sending loop, and a host's threads (028)eunhhu2026-10-016-7/+211
| | | | | | | | | Five rounds of 1,000 bursts by one wire, four wires, one link and four links: a thread each (966b82f), links on the loop (94e7bb8), wires on it too (5f5603c). Medians: one wire 774k, 782k, 779k sends/s; four wires 629k, 625k, 615k (the same CPU a send); one link 581k to 948k; four links 485k to 811k. A host whose app sends to ten links and ten wires has 24 threads before and 5 after (3 with no output).
* Measure one sending loop against a thread a link, with and without push (028)eunhhu2026-10-017-0/+338
| | | | | | | | Five rounds of 1,000 bursts by one link, four links and one wire, four builds in turns: a thread a link (966b82f), those threads with push (threads-push.patch), the loop (99b2219), the loop with push (94e7bb8). Medians: one link 597k, 923k, 525k, 897k sends/s; four links 506k, 799k, 268k, 800k; the wire as it was, 789k to 794k.
* Push a turn's sends to a link at once, and round only the links that want ↵eunhhu2026-10-012-3/+5
| | | | | | | | | | | | one (017, 2b-1) The sending loop was woken by every record queued while it slept: the owner queues one by one, the loop caught up and slept between them, and every link took a round on every wake. Four links sent 64k packets of 12 records where threads sent 40k of 20, and ran at 257k sends/s against 581k. The owner now queues a turn's sends and pushes each link once; the loop rounds a link only when its socket has answers, it was pushed, or its time came.
* Measure sending between two apps of a host: local outlet or link (112)eunhhu2026-10-016-0/+166
| | | | | | | | | | Two apps of one host, src sending one message for each of 1,000 items in a burst event, 2,000 bursts a run, five rounds with the ways in turns: the local outlet stores 1,141,642 sends a second at 1.17 us of the host's CPU each, a loopback pure:// link to the host's own port 578,879 at 2.93 us (1.97 times the sends, 0.40 times the CPU). 017's second stage asked for at least the link's rate at half its CPU or less: it goes in. Every run checks dst's sum: each send came once.
* Measure one host of many apps against a process each (111)eunhhu2026-10-0158-0/+546
| | | | | | | | | | | | | | | | | | | | 017's first stage met its four criteria on the Pi (RAM stores, five rounds, the order of the ways turned): app B's 99th percentile while app A is flooded by 64 connections is 168 us, half of two processes' 336 us; the two apps busy together write 146.1k a second against 146.2k, with the same 1.4 ms tail; an app costs a host 120 KiB of its own memory (a process: 1,311 KiB PSS) and 100 apps still run on three threads; a host of one app keeps its speed (TPC-C +1.2% at the median of six pairs, two of them interfered and kept; the bank's queries within 2.6%) and its instructions (+0.94% at most). On the way: apps hopped between workers each turn (fixed in db4ee9c), connections of one app spread over all connection threads made the host a third more expensive a request and its tail a kernel tick (fixed by home threads, d24881d), and the work budget's counting cost every table row 26 instructions (fixed in be297ea). The earlier runs are kept apart (db4ee9c's isolation, d24881d's pairs and counts, the interrupted first try).
* Measure 07047c3 again on a quiet Pi (110)eunhhu2026-09-3025-0/+96
| | | | | | | | | | The six pairs rerun once the Pi had been quiet for a minute, on the same binaries. Two pairs were still interfered (the first's before at an owner share of 0.54, the sixth at 0.73 on both sides); the four clean ones are -1.7%, -0.4%, +1.8% and +1.1%, CPU per transaction 43.25 to 42.9 us. The bank's queries are as fast or faster after, though the interference fell more on the before runs; the instruction counts stay the comparison to trust.
* Count instructions where timing cannot tell 3% apart (110)eunhhu2026-09-3062-0/+718
| | | | | | | | | | | | | | | | | 016's second stage passed its criterion: six interleaved pairs against ee21aec gave TPC-C (RAM, W=1) -0.6% at the median and the bank's queries 0.0% to +2.2%, no run interfered. The ranks query was slower all six times, and chasing it showed that on this Pi timing cannot separate 1-3%: two servers asked in turns disagree with themselves (ee21aec to e02bfc9 +1.6%, e02bfc9 to 481642f -1.2%, ee21aec to 481642f +2.8%), and a query whose instructions grew 0.6% ran 3.5% faster. Instructions do not move so: pcount.c counts the server's user-mode instructions with perf_event_open (no perf here), and psample.c samples where they go. ranks had grown 0.4% in instructions (the rest was where code and data landed), per_branch 3.6% and tables 20 instructions a row, which 07047c3 halved. The six pairs of 07047c3 were interfered (another session's build, load 4-12) and are kept apart, unused.
* Say that prefetching items did not pay, and why (109)eunhhu2026-09-3017-0/+201
| | | | | | | | | Fetching a found item's cells, a leaf's keys and each level's keys ahead made finding and reading items 20% faster on 100,000 items in a micro benchmark, but TPC-C's clean pairs moved 1% at most and finding alone got 48% slower, so it is not in. The owner's finding samples were the descent, not the fields: a write right after a read copies the leaf whole anyway, and what costs is walking structures larger than the caches at random.
* Say what snapshots cost the owner, and what reusing their nodes gave (107)eunhhu2026-09-3024-2/+259
| | | | | | | | | Most of it was the allocator: leaves past glibc's thread cache, freed by whichever thread let the last snapshot go. jemalloc was 11-14% faster at more memory; reusing the nodes in Pure gave a median +9.4% over six pairs, 4-8% less CPU a transaction in all six, and 1.97x the state bytes resident where it was 2.18x. TPC-C in RAM is now 4.0x a tuned SQLite at W=1 and 3.5x at W=4; finding items is the owner's largest part now.
* Find pure-serve's owner by its thread, and sort its samples as 106 doeseunhhu2026-09-301-45/+74
|
* Say what TPC-C costs when SQLite commits in groups too, and with four ↵eunhhu2026-09-3019-0/+372
| | | | | | | | | | | | | warehouses (106) SQLite tuned first, by the criteria committed before (and, beyond them, checkpoints each 40 MB of WAL, in its favour). On the SD card Pure stays 4.2-10.8x ahead at equal terminals, SQLite writing 8-18x the bytes a transaction; in RAM 3.2-3.5x. All tables equal at W=1 and W=4, and every run consistent. In RAM the one owner is the limit (0.94 of a core at W=4, no gain from 16 to 64 terminals), a third of its busy time copying nodes the snapshots share; checkpoints and memory are within their criteria.
* Say what the windows and checkpoints cost (013 steps 2 and 3, 104)eunhhu2026-09-303-8/+22
| | | | | | | | | | | | | | | | | | 013's second step is in (d9e0b68): each transport holds a window of its output's sends. 104 measures it with the receivers down: once the window is full a node grows by the outbox alone, 219 and 218 B a send over the link and HTTP (399 and 534 B before). The third step is a measurement. A checkpoint holds one list's snapshot while it writes the image, so what it adds is what the app changed meanwhile: nothing visible on tmpfs or for a list that fits the card's fast writes; on the SD card, a 22.4 MB list written for about 8 s while 1,400 items a second were set left +3.0 and +4.4 MiB. The highest resident memory was twice the state bytes, inside 013's bound (the state and one snapshot) and its stop criterion (3x). 021's run.py takes --items, --stores (a directory on the device to measure) and --label, and reports the peak (VmHWM).
* Say where a node's memory goes, and the budget it now keeps (104, 013)eunhhu2026-09-305-0/+261
| | | | | | | | | | | | | | | | | | 104 measures a node's memory under a receiver that is down and under a snapshot held for long (021's run.py: resident memory against what was posted, 30 s a case). A held snapshot grew the node without end (6.4 to 678 MiB, fixed in d1e8f38); a slow receiver grows it by what was sent, 221 B a send, which is state until delivered, so the budget bounds state and refuses past it. 013 is that budget's design: what it counts (state bytes, deterministic), what an event over it does, why an answer takes its send's room, how links and HTTP wait, what it does not count and those bounds, and the checks. With --memory 4000000 the same load fills it in about 4 s and the node stays at 9.6-9.8 MiB (45.9 MiB and growing without). 012 records the hole the budget found in the link (the full flag), and 103 that its clock fix fell short (385754c). CHECKPOINT gains the row.
* Say what adopting the link took, and how it fares against ngtcp2 (102)eunhhu2026-09-291-0/+124
| | | | | | | | | | | | | | | | | | 102: the spec (012) and a receiver written from it alone; the four holes writing it found (REFUSED, the record's size, a peer's run places, the memo's places beside) and the credit M6 showed fixed at 1,024 records; M6 with ngtcp2 as a third, two rounds. Under reordering Pure carries the most (45 Mbit/s; ngtcp2 29-47, quinn 1-2), under random loss the three are the same, on a clean bottleneck Pure carries 3-5 % less (its framing, some 112 B a packet to QUIC's 41), and where loss and reordering meet ngtcp2 carries 1.3-3.6 times as much: undoing a loss it took for reordering, it restores the window a real loss had cut too (its log: 22 of 34 undos past the window before the last cut; none without reordering). Shares beside TCP stay within 25 %. 011 and the status table say so; 012 also says that a receiver answers within 25 ms (a sender's PTO counts on it), that a shape and a record's bytes are the app's, and why a packet of the past does no harm.
* Let ngtcp2's handshake through a path that reorderseunhhu2026-09-291-1/+8
| | | | | | | | OpenSSL 3.5's ClientHello offers ML-KEM with X25519 and takes two Initials; when the path brought the second first, ngtcp2's server was asked for a Retry and the harness gave up. The client now offers X25519 alone, as Pure's links agree, and the server lets go of a connection it may not keep, so the client's next Initial begins another.
* Put ngtcp2 beside quinn in M6, as it is in M5eunhhu2026-09-292-66/+210
| | | | | | | | | drain.c takes a rate and seconds as quinn's client does (paced messages stamped with the time they were written, and no more written past the seconds), and its service writes quinn's line for each stream: the messages read in each 100 ms and how long they took. 020's run.py runs it as a third, on port 40005 behind the same bottleneck, and --who picks whom to run.
* Put ngtcp2 beside quinn in M5, as the design plannedeunhhu2026-09-294-0/+505
| | | | | | | | | | | | | ngtcp2 1.25.0 with OpenSSL 3.5's QUIC TLS (its ossl backend), fetched and built into build/third by ngtcp2/build.sh; drain.c does in C what the quinn harness does: one connection, one stream, "pure" and then the messages each after its u32 length, counted by the service, packets in GSO bursts. Its stream bytes are a function of their offset, so what ngtcp2 keeps to send again points into a pattern that never changes. run.py's drain_ngtcp2 measures both sides' CPU as drain_quic does. Five rounds each, both sides' CPU a message: 61 B Pure 0.280, ngtcp2 0.270, quinn 0.310 us; 1 KiB 2.62, 3.54, 3.94; 16 KiB 40.2, 54.4, 60.2.
* Say LOST when a run's epoch is not known, and let its sender begin again at onceeunhhu2026-09-291-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | A receiver that started again without its memo could open none of its senders' packets (their epochs were gone), said nothing, and each sender waited 2 s and four probes, then offered with no records. Worse, those packets failed to open, so each cost a token (256 a second, the flood guard): ten thousand senders came back 256 a second. M2 without a memo stopped 0.8 s and caught up in 7.7 s. Now the receiver answers a packet of a run it does not know, in an epoch past 0, with LOST: sealed in epoch 0 under the nodes' keys, naming the packet. As QUIC's stateless reset, it is smaller than the packet and comes out of a bucket of its own (4,096 a second). A sender that has that packet out begins again as a run begins: epoch 0 with its records at once, epoch 1 offered. Only the receiver can say LOST, and one of the past names a packet no longer out; silence alone (an attacker's doing, maybe) still gets offers only. Such a packet costs no key token: no keys were made for it, and the secret made is remembered, once a sender. M2 without a memo (ten thousand devices, twice): no 100 ms step stopped, caught up in 0.4-0.5 s, p99 5.8-5.9 ms, the most 48-57 ms (was 4.1 and 7.8 s). test-link: a changed packet may get a LOST smaller than itself and nothing else; half of a flood in epoch 1 got 267 LOSTs, 12 KB for its 6 MB; a record goes under the nodes' keys after an epoch began only after the receiver forgot, until the next epoch.
* Say that a receiver moved to another machine is followed, and how (100)eunhhu2026-09-291-0/+4
| | | | | | | | | | | | | H3 of design 011 stands: with ten thousand devices sending 2,000 records a second, the node's store, key and memo carried to another address, nothing stopped for a 100 ms step, the moved node made no X25519, and every record was applied once. Each device's first record after the move waits one probe (30-45 ms here), as its first packet finds no way there. The search found the listener answering from an address other than the one a packet came to, and a link that looked for its receiver after its probe went, losing every answer. H1-H4 are all measured now; 61 B's CPU a delivery is at the edge of the criterion, to be looked at before step 5 (adoption and a specification).
* Move M2's node to another machine under load (M3's harness)eunhhu2026-09-292-11/+88
| | | | | | | | | | run.py --restart --move runs in namespaces of its own (unshare -rmn: a network and an /etc/hosts): the devices send to receiver.test, which names 127.0.0.2; at --kill-at the node is killed, its store, key and memo copied elsewhere, 127.0.0.2 made unreachable, the name pointed at 127.0.0.3, and the copy started there. The load generator's devices look for the node again as link/link.c does: after three probes in a row unanswered, or a send the system said had no way there.
* Keep a sender's congestion state as one, so an episode undoes it wholeeunhhu2026-09-291-2/+2
| | | | | | | | The window, threshold, the start of recovery and CUBIC's four values were fields of their own, and an episode of congestion events copied each of the seven aside and back. Now they are one Congestion: an episode keeps a copy, and undoing it takes the copy back, the window grown since kept. test-link's results and the M6 simulator's are the same to the byte.
* Say what epochs cost a link, and what the search for it found (099)eunhhu2026-09-294-0/+87
| | | | | | | | | | | | | | Forward secrecy, 098's first task left: keys that leak later open only epoch 0. M2 after the epoch began and after a restart is as before it (p50 0.4 ms, p99 0.8 ms, the same CPU, no X25519 on restart). It costs a p50 of 0.7-1.0 ms more while ten thousand devices first meet and for about 40 s of accepts after, and a receiver that lost its memo now stalls 0.8 s and catches up in 7.7 s, not 0.2-0.7: it lost the epochs, and each sender opens one again. The search also found a packet more a device from each of M6's path probe and the security checks' challenge, and a load generator whose own pass over its devices was the floor of the latency it measured. 098's note that epochs narrow KCI to the first epoch was wrong; corrected.
* Move a link to a new epoch now and then, so keys that leak later open ↵eunhhu2026-09-291-3/+4
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | nothing past A sender offers the public half of a pair made for the next epoch (OFFER) until the receiver accepts with a half of its own (ACCEPT); their X25519 and the epoch's secret make the next one's, and the private halves go. The header's low four bits say a packet's epoch; epoch 0 is the nodes' static secret, as the keys were. The first epoch is offered in the run's first packet (it begins with the first answer when the receiver is not busy), then one an hour or GiB. One who later has both nodes' keys opens only epoch 0, and no record went there after an epoch began: test-link tries every packet sent under the static keys, with epochs a second long, loss, reordering, restarts and replays. The receiver writes the epoch it accepted in its memo (a place is 256 bytes now), so it goes on without an X25519 when it starts again; one that forgot is not heard from, and its senders, after 2 s and four probes unanswered, offer again in epoch 0 with no records until an epoch begins. Keys of four epochs are tried (accepted, now, before, zero), as two may share the four bits. An accept costs an X25519 on each side, 220 µs on this Pi: one of a sender with records costs a token (256 a second, 8 at once) and the next packet offers again when there is none; one of a sender with nothing else to send waits for none, one a second for it. The X25519 is made after the turn's answers, never between a packet and its answer. serve's report and M2's generator count keys, X25519s and epochs.
* Give M2's devices a timer each, and say when the slow records were madeeunhhu2026-09-291-7/+34
| | | | | | | | | | | The load generator asked all ten thousand senders for their timers every 5 ms: 83 % of its CPU (47 ns a sender, mostly cache misses), and records were timed from the start of that pass, so the pass (0.75-0.94 ms, longer as the sender grew) was the floor of every latency it measured. A device now keeps its sender's next timer, asked again only when the sender is touched (the only time it changes), and the p50 of M2 falls from 0.8 to 0.4 ms for every version alike. It also says the p50 and p99 of the records made in each second, and the keys and X25519s its senders made.
* Measure a link on paths of loss, reordering and a bottleneck, against quinn ↵eunhhu2026-09-291-0/+126
| | | | | | | | | | (097) M6 of design 011: with a 50 Mbit/s bottleneck, a link carries 21-41 times quinn's goodput when packets reorder, the same under random loss, and 1.3-3.5 times with both; its p99 at 50 records a second is the same or shorter; beside TCP CUBIC it takes 0.48-0.55 of the path, inside the guardrail. Only on a clean path is it 3-5% below quinn.
* Let M6's path waver by 1 ms when flows share it, and count from 20 s of 60eunhhu2026-09-291-6/+9
| | | | | | | | With a fixed delay the drop-tail queue dropped one flow's packets eight times as often as the other's by their phases alone (Floyd and Jacobson, 1992): Pure took 0.63-0.77 of the path from TCP and 0.69 from quinn, 0.47 from itself, and 0.49 under fq_codel. With 1 ms of jitter it takes 0.53 at an RTT of 20 ms.
* Record one round of M6 with the path's MTU found, before pacing (020)eunhhu2026-09-291-0/+42
|
* Let the M6 simulator queue records at a rate and time each oneeunhhu2026-09-291-5/+33
|
* Start M6's flows together, each a process, and count their shares from 10 seunhhu2026-09-291-26/+63
| | | | | | The TCP flow started first and in a thread of the runner; two TCP flows split 0.31 to 0.56. Now every flow is a process started at once, run 40 s, counted from 10 s on.
* Find how large a packet the path carries: 1,452 bytes once a probe is ↵eunhhu2026-09-292-4/+4
| | | | | | | | | | | | | | | acknowledged A link starts at 1,200 bytes; once an RTT is known it sends a PING padded to 1,452 (DPLPMTUD, RFC 8899), alone and with the don't-fragment bit, and uses that size once it is acknowledged. A probe lost is no congestion; it is tried again a second on, three times, then ten minutes on. GSO is asked for each burst (a control message), not the socket, so a packet sent alone is never cut. CUBIC counts in the datagram. On loopback packets fall by 19-27%; the CPU a record falls 13% at 1 KiB and 7% at 16 KiB; in the simulator of a 50 Mbit/s path with 1% loss the goodput rises from 6.1 to 7.0 Mbit/s (5%: 2.3 to 3.0).
* Record M6 with records in parts and no path MTU yet (020)eunhhu2026-09-291-0/+126
|
* Queue only the way to the receivers behind M6's bottleneckeunhhu2026-09-292-18/+30
| | | | | | | | tbf with netem under it lost tbf's queue (netem became it), and the answers queued behind the data. Now each way has its netem, the way to the receivers' ports then a tbf whose queue holds a round trip; the way back does not queue. transport.c takes its listener's port (TRANSPORT_PORT).