| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
| |
(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.
|
| |
|
|
|
|
|
|
| |
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.
|
| | |
|
| | |
|
| |
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| | |
|
| |
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
| |
From the one after the last heard, letting be the ones heard before and
stopping at a gap: the listener's first delivery may begin past it when
packets reorder, which the harness counted as wrong.
|
| |
|
|
|
|
|
|
|
| |
020-lossy-paths/run.py runs each case in an unprivileged namespace: a
50 Mbit/s tbf bottleneck under netem's delay, jitter and loss, both
ways. A link may send without UDP GSO (pure_link_gso), as netem takes a
burst as one packet; quinn the same (QUIC_GSO=0). transport.c and the
quinn harness give the records handed over in each 100 ms, and at a
steady rate the time each took; TCP CUBIC flows run beside them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A packet called lost stays known a while; acknowledged after all, it
came late: what it carried counts as received, the reordering awaited
grows (later packets acknowledged, and time past the RTT, as RACK), and
an episode of congestion events whose losses all came late is undone.
The window grows only when it was used (RFC 9002 7.8), and congestion
avoidance is CUBIC (RFC 9438), as quinn's.
In a path of 50 Mbit/s whose packets reorder by up to 5 ms, with no
loss, a link carried 1.5 Mbit/s at an RTT of 20 ms and 10 at 100 ms; now
43.6 and 40.1, as without reordering (research/experiments/020-lossy-paths,
virtual time). With 1% random loss: 4.9 to 6.1 at 20 ms, 1.0 to 1.3 at
100 ms. The transport's CPU a record is unchanged.
|
| |
|
|
|
|
|
|
|
|
| |
links.c was a second implementation of the packet format; each device is
now a PureLinkSender on a socket of its own, keyed with a secret made
outside (pure_link_sender_shared: a key kept elsewhere, or a test's
many). M1 (1k, 10k) and M2 (10k, kill -9) pass with it: no silent step,
caught up in 0.2 s, every order applied once; the slowest store after
the restart is now the sender's 1 s wait for a word of the save
(STORED_WAIT), where the old devices tried again after 200 ms.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
link/link-protocol.h is the protocol with no socket, thread or clock of
its own: packets and time in, packets out. link/link.h carries it over
UDP as before. A sender starts small (8 flights, 16 records, a 4 KiB
ring) so one thread can hold many. The transport's CPU a record is the
same within 2% (five rounds each way, 61 B to 16 KiB).
test-link runs both sides through a network of its own: loss, packets
held back and doubled, failed saves, an idle sweep, and both sides
starting again, every record applied once as sent.
|
| |
|
|
|
|
| |
A sender takes its messages' shape, and a listener asks its host which
input a stream goes to. The core gives the shape (pure_module_shape);
link/ now builds without the core's headers.
|
| |
|
|
|
| |
libpure-link.a holds them; pure-serve and the M5 harness link it instead
of naming its source files.
|
| |
|
|
|
|
|
| |
Between two pure-serve nodes, 16 KiB records drain at 33,600 a second
(23,000 before), the links spending 58 us a record (76), under quinn's 60
while the receiver also stores each one. 1,013 bytes: 406,000 a second
(347,000). Saving sends one by one: 5,000 of 16 KiB in 0.91 s (84.8).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
M5 of design 011. At first a link spent eight times quinn's CPU on a small
record: scans of the backlog, a system call and allocations a record, 19
bytes of framing. After the seven changes before this one, the transport
alone (transport.c; a backlog drained, every record checked) spends per
record 0.30 us for 61 bytes (quinn 0.27: 1.06 to 1.12 times over three
campaigns, at the design's 10 % line), 2.91 us for 1,013 bytes (quinn
3.88) and 45.2 us for 16,013 bytes (quinn 60.9): 25 % less for the larger
ones, and 30 % more records a second. What is left for small records is
per packet: the stream's names and the RECORDS head. quinn's numbers carry
no confirmation of storing and no exactly once.
Between two pure-serve nodes the links spend more per record, woken at
the receiving app's pace. Filling the sender showed the outbox is written
whole at every save, as the numbers heard were (094): the next thing to
change.
run.py gains --transport and --drain; the quinn service reads a drain a
chunk at a time and its client writes 64 KiB at a time.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A record over a piece (1,000 bytes) went in pieces even when it fit the
packet whole: a record of 1,013 bytes went as two RECORD frames, and the
receiver gathered it the slow way (an allocation, pieces, then onto the
line). A record none of whose pieces went yet now goes whole in a RECORDS
frame whenever it fits what is left of the packet; its pieces are marked
sent together. A record too large for a packet, or one resent after
some of its pieces were lost, still goes in pieces, and a receiver takes
a record whole or in pieces alike.
The transport alone takes 2.85 us a record of 1,013 bytes, not 3.18
(quinn: 3.90). transport.c now checks every record delivered: in order,
once each, its bytes as sent. None was wrong in a network namespace with
1 ms of delay, 3 % loss and 10 % reordering, with records of 61, 1,013
and 16,013 bytes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
M2 and M4 of design 011. Ten thousand devices send an order every five
seconds (2,000 a second); the receiver is killed (-9) at about 21 s and
started again at once. With its secrets remembered the node shows no
100 ms step without an order stored, catches up in 0.2 s, spends no more
CPU than before and makes no X25519; 34 orders were sent again, and
every order made was applied once (three rounds; on the SD card the
same, with 0.2 s silent). Without the memo it makes 10,000 X25519, 1.6 s
of CPU. quinn under the same load stops for 1.0 s and catches up after
10.9 s: 10,115 new connections, about 7 s more of the service's CPU and
8 s of the clients'. A first contact takes Pure one round trip and QUIC
two, at 10, 50 and 100 ms of RTT.
run.py --restart and --first-contact make them; the quinn harness gains
a fixed port with a reset key from its key, clients that connect again,
and a drain for M5.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| | |
|
| |
|
|
| |
and an SD card
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|